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

C++ 类构造时隐式转换的小疑惑

  •  
  •   Tony042 · 2020-05-24 11:57:20 +08:00 · 3031 次点击
    这是一个创建于 1645 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在学 C++的相关知识,在学习类模板参数的辅助自动推导时,有了些疑问,请看下面代码:

    template <typename T, typename Cont = std::vector<T>>
    class Stack
    {
    private:
        Cont elems;
    public:
        Stack() = default;
        Stack(Stack const &) = default;
        Stack(T const &elem) : elems({elem}) {}
    };
    
    Stack(char const *)->Stack<std::string>; // CATD
    

    在构造这个类的时候:

    Stack stringStack = "bottom";  // MSVC  16.9 编译通过,GCC-10 编译不通过
    Stack stringStack{"bottom"};   // MSVC  16.9 和 GCC-10 均编译通过
    

    请问为什么第一个构造方式 GCC 编译不通过呢,是否是因为 MSVC 又启用了某些魔改插件?个人理解是从 Char const[7]或 char const* 转换到 string 算一次转换,然后从 string 到 Stack 进行了二次转换,而 C++只允许进行一次隐式转换,不知是否正确

    第 1 条附言  ·  2020-05-24 13:21:17 +08:00
    另外还有一个问题,Stack(T const&)这里是引用传递不是值传递所以不应该触发 char const[7]到 char const *的类型衰变( type decay ),但是很奇怪的是这里 GCC 和 MSVC 都编译过去了,没有报错。
    26 条回复    2020-05-27 09:18:12 +08:00
    nightwitch
        1
    nightwitch  
       2020-05-24 12:44:47 +08:00
    第一个函数你是从"const char [6]" 类型转换到 Stack<std::string>类型, 这个类型系统肯定会报错的, 这也不是构造函数,而是 operator=函数.
    Tony042
        2
    Tony042  
    OP
       2020-05-24 12:47:47 +08:00
    @nightwitch operator= 是 copy assignment 函数,我这里就是新构造一个 Stack 类型的 stringStack,没有赋值操作,事实上我在写这个例子时候把 operator=delete 的了,所以这里就是构造函数,之所以不用写类型参数,试用了 C++17 的自动推导,我很疑惑为啥俩编辑器一个通过一个不通过
    Tony042
        3
    Tony042  
    OP
       2020-05-24 12:49:39 +08:00
    @nightwitch 不好意思还要再杠一下,是 const char[7]不是 const char[6]因为这个数组最后会自动补一个'\n'
    lechain
        4
    lechain  
       2020-05-24 13:11:01 +08:00   ❤️ 1
    很久没接触过 C++了,提个我自己的猜测。

    写法二一定会走 “Stack(T const &elem) : elems({elem}) {}”构造的,这个是显示声明实现了的,所以铁定是没有问题的
    写法一是走的 fault 构造的,二 default 构造你并没有显示实现,所以这个构造得依赖于编译器,编译器以不一样的方式隐式的实现,就会产生不一样的结果

    、char*格式的 string 和 C++ class String 自动类型转换也依赖于其默认构造函数,这又是一个依赖于编译器隐式实现的默认构造

    你也说了,写法一存在两种类型转换,但是考虑到这两种转换都是隐式转换,很显然你无法对依赖于编译器实现的隐式转换报有过多的期望。
    damingxing
        5
    damingxing  
       2020-05-24 13:14:33 +08:00   ❤️ 1
    不用纠结,直接用第二种方法就行了。。
    Tony042
        6
    Tony042  
    OP
       2020-05-24 13:20:30 +08:00
    @lechain 谢谢你,其实还有个问题 Stack(T const&)这里是引用传递不是值传递所以不应该触发 char const[7]到 char const *的类型衰变( type decay ),但是很奇怪的是这里 GCC 和 MSVC 都编译过去了,没有报错
    constexpr
        7
    constexpr  
       2020-05-24 13:31:09 +08:00   ❤️ 1
    CTAD 太复杂, 不充分了解 CTAD 是没法很好回答这个问题的- -
    那个我认为应该是 GCC 的一个 BUG, 他在拷贝初始化的时候没有使用用户定义的推导指引.

    你的第二个问题, 推导指引是这样工作的,他选择了一个合适的推导指引(用户定义或者隐式的),然后推导出类型,这里是 std::string, 然后把这个类型带到类里面,那么这里的 T 就是 string 了, 所以 那个构造函数是 Stack(const std::string &elem)... 你可以用 typeid 确认一下确实是这个类型.
    constexpr
        8
    constexpr  
       2020-05-24 13:31:38 +08:00   ❤️ 1
    @constexpr 是 CATD...
    constexpr
        9
    constexpr  
       2020-05-24 13:35:06 +08:00   ❤️ 1
    @constexpr whatever. 反正是用户定义的推到指引

    C++17 这个功能具恶心,
    std::vector a{1, 1.2}合法,
    std::vector b{1.2, 1}报错
    你想想这是为什么
    lcdtyph
        10
    lcdtyph  
       2020-05-24 13:35:19 +08:00 via iPhone
    @constexpr
    是 CTAD
    secondwtq
        11
    secondwtq  
       2020-05-24 13:36:15 +08:00   ❤️ 1
    secondwtq
        12
    secondwtq  
       2020-05-24 13:40:23 +08:00   ❤️ 1
    另外第一个是 copy initialization
    const char *和 std::string 之间的转换是标准定义的,不是 implementation-defined
    constexpr
        13
    constexpr  
       2020-05-24 14:03:57 +08:00   ❤️ 1
    @constexpr 我重新审视了一下,第一个 GCC 或许不是 BUG
    因为是拷贝构造, 所以等号右边得是 Stack 类型, 而他根据用户定义是推到指引, 最终会引发 char const[7]->String->Stack 的转换,这是不允许的.

    PS: 数组最后一位是'\0', 而非'\n'
    msg7086
        14
    msg7086  
       2020-05-24 14:17:01 +08:00
    @constexpr
    GCC
    1.cpp:19:23: error: conversion from 'const char [7]' to non-scalar type 'Stack<std::__cxx11::basic_string<char> >' requested

    Clang
    1.cpp:19:9: error: no viable conversion from 'const char [7]' to 'Stack<std::__cxx11::basic_string<char>, std::vector<std::__cxx11::basic_string<char>,
    std::allocator<std::__cxx11::basic_string<char> > > >'

    看上去的确是发生了 const char[] 到 Stack 的直接转换。
    Tony042
        15
    Tony042  
    OP
       2020-05-24 21:37:41 +08:00
    @constexpr 谢谢指正,是'\0'
    Tony042
        16
    Tony042  
    OP
       2020-05-24 21:40:36 +08:00
    @constexpr #9 能解释下为什么 std::vector a{1, 1.2}合法, std::vector b{1.2, 1}报错么
    Tony042
        17
    Tony042  
    OP
       2020-05-24 21:42:11 +08:00   ❤️ 1
    @msg7086 这个地方,应该是把 Stack(T const &elem)改成 Stack(T const elem)就可以编译通过了,但是 gcc 如果加上-std=c++2a 的话用 ref 版的也可以通过
    Tony042
        18
    Tony042  
    OP
       2020-05-24 21:51:25 +08:00
    @secondwtq 你好,谢谢提供的资料,我看了下,貌似这个地方不是 user-defined conversion,由于 Stack 这个类是个模板,在 C++17 以前必须写成 Stack<std::string> s("bottom"),显式指明类模板参数,但是 C++17 支持 class template argument deduction,所以在一部分情况下编译器会自动推导类模板参数,但是也会经常推导出错,比如如果没有最后一行类模板推导指引,一般来说编译器会将 T 推导至 char const[7]而不是推向 std::string,这样的话就会带来很多麻烦。
    Tony042
        19
    Tony042  
    OP
       2020-05-24 21:59:09 +08:00
    @secondwtq 刚才又想了下,你说的是有道理的,谢谢
    constexpr
        20
    constexpr  
       2020-05-24 22:23:31 +08:00   ❤️ 1
    std::vector a{1, 1.2}
    他将采用隐式的类型推导指引(implicitly-generated guide), std::initializer_list<T>这个行不通, 但是
    vector( size_type count, const T& value, const Allocator& alloc = Allocator());
    确是可行的, 这里 T 被推导为 double 类型, 然后把这个类型带进去, 发现 std::initializer_list<double>是更好的选择, 于是调用这个构造函数(即 vector( std::initializer_list<double> init, const Allocator& alloc = Allocator() );).


    当初那个帮他推导的指引所对照的构造函数却没有调用, 所以类似的 std::vector a{10, 1.2} 是一个有 2 个元素的 vector, 而非是有 10 个元素且默认值为 1.2 的 vector
    Tony042
        21
    Tony042  
    OP
       2020-05-24 22:27:53 +08:00
    @constexpr 这玩意真的好复杂,不知道这编译器都是咋写出来的
    Wirbelwind
        22
    Wirbelwind  
       2020-05-26 12:39:37 +08:00   ❤️ 1
    这个推导指引被视为 函数了
    Wirbelwind
        23
    Wirbelwind  
       2020-05-26 13:26:41 +08:00   ❤️ 1
    @Wirbelwind 之前只看了提示报错,测试不充分

    ---
    Stack stringStack{"bottom"};
    这句可以过是因为 Stack(T const &elem),这里定义的第三个构造函数,如果把去掉是编译不过的 提示也是类型转换问题。

    ---
    这里的问题主要是 模板生成时不允许强制转换
    Wirbelwind
        24
    Wirbelwind  
       2020-05-26 14:02:15 +08:00   ❤️ 1
    Stack(char const *)->Stack<std::string>
    这里因为你指引了一个模板 所以应该提供一个专用的 构造函数。

    建议不要学模板这么复杂的功能,心智负担太大。
    以上分析不保证正确
    Tony042
        25
    Tony042  
    OP
       2020-05-26 23:18:46 +08:00
    @Wirbelwind #23 是这个构造函数在起作用,我疑惑的地方是,传入引用 ( T const& elem)为什么可以发生 type decay 也就是 char const[7] -> char const *,一般只有传入值的时候会 type decay
    Wirbelwind
        26
    Wirbelwind  
       2020-05-27 09:18:12 +08:00
    @Tony042 生成模板代码的时候 那里固定成了 std::string,但是 std::string 好像没有数组的构造函数,然后转换成了 char*

    大概是这样
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2983 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 14:09 · PVG 22:09 · LAX 06:09 · JFK 09:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.