Raiscies

导航

 

[文章存档 - 发表日期:  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 消歧义符" 一块也还有几个不同的使用场景,大家可以看一下.

posted on 2021-12-24 17:25  Raiscies  阅读(287)  评论(0)    收藏  举报