V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Tony042
V2EX  ›  C++

模板类内模板构造函数的重载问题

  •  
  •   Tony042 · 2020-08-12 08:29:28 +08:00 · 2146 次点击
    这是一个创建于 1324 天前的主题,其中的信息可能已经有所发展或是发生改变。

    不好意思问题题目有点绕,请看以下类模板代码

    #pragma once
    #include <functional>
    #include <type_traits>
    
    template <typename... Types>
    class Tuple;
    
    template <typename Head, typename... Tail>
    class Tuple<Head, Tail...>
    {
    private:
        Head head;
        Tuple<Tail...> tail;
    public:
        Tuple() {};
        Tuple(Head const& h, Tuple<Tail...> const& t) : head(h), tail(t) {}
        Tuple(Head const& h, Tail const& ... t) : head(h), tail(t...) {}
    
        template <typename VHead, typename... VTail> \\ 构造函数 1 
        Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
    
        template <typename VHead, typename... VTail> \\ 构造函数 2
        Tuple(Tuple<VHead, VTail...> const & other) : head(other.getHead()), tail(other.getTail()) {}
    
        Head& getHead() {
            return head;
        }
        Head const & getHead() const {
            return head;
        }
        Tuple<Tail...> & getTail() {
            return tail;
        }
        Tuple<Tail...> const & getTail() const {
            return tail;
        }
    
    };
    
    
    template <>
    class Tuple<> {};
    

    当我们试图以一个 Tuple 类型来构造另外一个 Tuple 的时候,有两种情况,一种是构造相同模板参数 tuple,另一种是构造不同但可以相互转换的模板参数,代码如下:

    Tuple<int, double, string> t(17, 3.14, "Hello world");
    Tuple<long int, long double, string> b = t;  \\ 情形 1
    Tuple<long int, long double, string> b = t;  \\ 情形 2
    

    在这个类模板定义下,上述代码是编译不过去的,原因是构造函数 1 的优先级比构造函数 2 高,所以编译器没有调用复制构造函数 2 。因此我们需要用std::enable_if_t去 disable 构造函数,所以构造函数 1 和 2 可以被改写为下面两种:

    第一种:std::enable_if_t为默认模板参数

    template <typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) > >
        Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
    
        template <typename VHead, typename... VTail, typename = std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) > >
        Tuple(Tuple<VHead, VTail...> const & other) : head(other.getHead()), tail(other.getTail()) {}
    

    第二种:std::enable_if_t为默认匿名 Nontype 模板参数

    template <typename VHead, typename... VTail, std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) > =0 >
        Tuple(VHead&& vhead, VTail&&... vtail) : head(std::forward<VHead>(vhead)), tail(std::forward<VTail>(vtail)...) {}
    
        template <typename VHead, typename... VTail, std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) > =0 >
        Tuple(Tuple<VHead, VTail...> const & other) : head(other.getHead()), tail(other.getTail()) {}
    

    那么问题来了:

    1. 为什么加了相同的 enable_if 条件,就使得构造函数 2 的优先级比构造函数 1 的优先级高了呢(构造函数 1 、2 的条件都是 VTails 类型的个数和 Tail 的类型个数相同。

    2. enable_if 的两种写法中第二种是CPP reference推荐写法, 原话是这么说的:A common mistake is to declare two function templates that differ only in their default template arguments. This does not work because the declarations are treated as redeclarations of the same function template (default template arguments are not accounted for in function template equivalence). 也就是说如果用第一种写法很可能会把构造函数 1 和构造函数 2 当成同一个函数声明和定义两次,当然本例中不存在这个问题,因为构造函数 1 和构造函数 2 在大部分情形下含有的参数个数不同,但至少这两种 enable_if 的写法是等价的。对于情形 1 构造相同模板参数 tuple 两种 enable_if 写法 gcc 和 msvc 均可以编译过去,但是对于情形 2 构造不同但可以相互转换的模板参数,只有第一种写法也就是默认模板参数方法,msvc 和 gcc 可以编译过去,这是为什么呢?

    感谢您看完如此长的问题描述,谢谢!

    编译器分别为 msvc 16.7 和 gcc 10

    第 1 条附言  ·  2020-08-12 09:01:30 +08:00
    感谢#1 的回答,我已经明白问题出在哪里了,具体原因请看#1 和#2 的回复
    13 条回复    2020-08-13 21:01:46 +08:00
    codehz
        1
    codehz  
       2020-08-12 08:49:28 +08:00   ❤️ 1
    你这情形 1 情形 2 不是一个东西么。。。
    对应构造函数 1 的应该是 b{17, 3.14, "Hello world"}
    1. 不是因为优先级高,而是因为构造函数 1 被吃掉了,enable_if 里不满足(俩 sizeof 结果不一样)。
    至于为什么不这样写就不可以通过编译,是因为没有 enable_if 的时候 VTail = <空> VHead = <Tuple<...>> 也是符合函数声明的,直到跑到初始化列表(指 head(std...那个)才会出现错误,这时候已经无法挽回了
    写了 enable_if_t 之后类型上 sizeof...(VTail) = 0 而 sizeof...(Tail) = 2 于是不相等直接 SFINAE 干掉了
    第二点我再看看。。。
    Tony042
        2
    Tony042  
    OP
       2020-08-12 08:55:32 +08:00
    @codehz 不好意思情形 1 我写错了,实际上是 Tuple<int, double, string>, 第一点让我再仔细读读您的回复,第二点我已经明白了,我的 enable if Nontype 那种写的有问题,改成 std::enable_if_t<sizeof...(VTail)==sizeof...(Tail),int > = 0 就好了,当时没仔细看 cpp reference 忘指定条件为真时转换成的类型了
    Tony042
        3
    Tony042  
    OP
       2020-08-12 09:00:22 +08:00
    @codehz 第一点你说的对,我可能当时绕进去了,没想明白,现在理解了
    Tony042
        4
    Tony042  
    OP
       2020-08-12 09:01:11 +08:00
    额,感谢#1 的回答,我已经明白问题出在哪里了,具体原因请看#1 和#2 的回复
    codehz
        5
    codehz  
       2020-08-12 09:06:49 +08:00
    好吧,第二点你理解错误了,按照 cppref 的写法,查找模板函数的时候类型参数会忽略默认值,于是这里第一种就是<typename VHead, typename... VTail, typename>这样的匹配模式(注意,这里和模板形参数的名字没关系),
    而第二种的匹配模式就是<typename VHead, typename... VTail, std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) >>
    然后,还是一样的。。。
    但是由于函数的参数列表不一样,所以和这个问题没有关联。(即使只有一个参数,VHead 推导的值也是不一样的,但是这里就是另外一个问题了。
    Tony042
        6
    Tony042  
    OP
       2020-08-12 09:15:47 +08:00
    @codehz 对的,我的理解也是这个理解,但是最好还是 std::enable_if_t<sizeof...(VTail)==sizeof...(Tail),int > =0 我刚才又试了下<typename VHead, typename... VTail, std::enable_if_t<sizeof...(VTail)==sizeof...(Tail) >>,gcc 可以编过去,msvc 编不过去,提醒这个 unamed parameter 没有使用,所以我觉得实践上可能 std::enable_if_t<sizeof...(VTail)==sizeof...(Tail),int > =0 比较好,std::enable_if_t<sizeof...(VTail)==sizeof...(Tail)> =0 会导致( void = 0)这个可能也是个问题?
    EggtartZ
        7
    EggtartZ  
       2020-08-12 17:38:37 +08:00
    看到好多次楼主的提问了,冒昧的问一下,楼主关于模板这方面的学习资料是哪些呢?谢谢!
    Tony042
        8
    Tony042  
    OP
       2020-08-12 23:58:12 +08:00
    @EggtartZ 我看的是 C++ templates 2nd edition. 作者写的挺好的,里面我有困惑的地方就发到 v2 上了
    PepperEgg
        9
    PepperEgg  
       2020-08-13 09:04:16 +08:00
    扫了一眼,确定是我工作中不敢写的代码了😆
    EggtartZ
        10
    EggtartZ  
       2020-08-13 09:50:14 +08:00
    @Tony042 感谢分享
    ftfunjth
        11
    ftfunjth  
       2020-08-13 13:42:29 +08:00
    一般我会这么写

    Tuple<int, double, string> t(17, 3.14, "Hello world");
    Tuple<long int, long double, string> b = t;
    return 0;
    ftfunjth
        12
    ftfunjth  
       2020-08-13 13:42:55 +08:00
    ```
    int main() {
    Tuple<int, double, string> t(17, 3.14, "Hello world");
    Tuple<long int, long double, string> b = const_cast<const Tuple<int, double, string> &>(t);
    return 0;
    }
    ```
    Tony042
        13
    Tony042  
    OP
       2020-08-13 21:01:46 +08:00
    @ftfunjth 为什么要这样再 cast const 呢,相比第一种会有什么好处?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2635 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 15:50 · PVG 23:50 · LAX 08:50 · JFK 11:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.