[文章存档 - 发表日期: 2020-11-22]
最近在玩c++的模板元,但是有一次编译器报了一个令人迷惑的错:
版本: g++ (MinGW.org GCC Build-2) 9.2.0, 标准为c++17.
test.cpp:23:36: error: expected ';' before '<' token
23 | using result = typename List::Push<T>::result;
| ^
| ;
故事的起因是这样的,我在写一个TypeList, 里面有一个元函数Push<typename T>,用来给TypeList尾端添加一个类型.
//一个简化了的TypeList定义
template <typename... Ts>
struct TypeList {
//对Type Node的定义在这里无关紧要, 因此忽略.
template <typename T>
struct Push {
using result = TypeList<Ts..., T>;
};
};
using list1 = TypeList<int, double, void>;
using list2 = typename list1::Push<bool>::result; //list2的类型为 TypeList<int, double, void, bool>
int main(){}
到目前为止, 一切都编译得很好.但是我要是想在 TypeList 的外部定义一个元函数,它接受一个 TypeList 实例化的类型 List 和要添加的新类型 T, 然后在这个元函数里再去调用Push<T> 呢?
template <typename List, typename T>
struct Invoker {
using result = typename List::Push<T>::result; //error!
};
//using list3 = typename Invoker<list2, char>::result; //这句即使不写上面也会报错.
于是乎就有了上面的报错信息,它似乎指示我们把 List::Push 之后的所有东西都删去, 像: using result = typename List::Push; , 事实上, 它确实能够通过编译, 可一旦把list3的那句给解注释, 代码还是得报错.因为我们定义的 TypeList 里面并没有 Push 这个嵌套类, 而只有 Push<typename T> 这个嵌套类模板.然而写一个简单的 Push 类而不是类模板是无法让我们添加新类型的.这个问题困扰了我很久,网上查也没查到什么结果,只能采取一些很绕的方法来解决.最后才在cppreference找到了正确的写法应该是这样:
//在作用域解析符:: 和模板名Push 之间添加 template 关键词.
using result = typename List::template Push<T>::result; //OK
这样就可以完美解决问题了.在这里, template 关键词的作用是消歧义,与 typename 作用类似.这和一个C++里的一个概念 待决名 有关.在这里, typename List::Push<T>::result 的结果取决于模板形参 List, 它在实例化之前是不定的, 也就是说编译器在当时并不知道这个List是什么类型, 更别说List里有什么成员, 什么内部类了. 而依赖于形参List的Push, 对于形如List::Push这样的东西, 编译器是优先把Push解析成是List里的一个静态(static)变量名, 函数名或内部类名, 而非是它的一个模板——尽管Push的背后跟着一对尖括号. 因此编译器一看到一个所谓的静态变量(或其他东西)后又跟着一对尖括号, 就直接报错了. 而若在Push之前加一个template, 就起到消歧义的作用, 使得编译器把Push当成是一个模板, 从而正确编译.
根据cppreference的解释, 在对依赖于模板参数的表达式进行有限定的名字查找(即存在作用域解析符)时,如果没有任何消歧义关键词,编译器会首先把它当成值待决表达式, 如:
using result = List::Push<T>::result,等号右边的表达式结果会被当成值而不是类型, 因此报错, 而类似这样的错误网上也都查得到,编译器也往往能够清晰地说是少了个typename关键词, 那么我们把它加上:
using result = typename List::Push<T>::result, 这样等号右边就指名类型了, 但还是报错, 因为编译器把List::Push<T>::result中的Push当成是静态变量/静态函数/内部类名而不是模板名, 而这些东西后是不能跟尖括号的,因此报错,需要加template指明Push就是一个模板名,这样才能成功编译. 对于缺少template消歧义的报错很隐晦, 不了解的人很难知道到底哪里错了.
有一说一, template 关键字还有这种用途是我没有想到的,这很c艹...
最后再来吐槽一下g++的坑爹报错信息,这种不明觉厉的信息谁找得到问题在哪...
参考自: cppreference-待决名. 其中"待决名的 template 消歧义符" 一块也还有几个不同的使用场景,大家可以看一下.
浙公网安备 33010602011771号