【问题标题】:C2440: 'initializing': cannot convert from 'A<double>' to 'A<double>'C2440:“正在初始化”:无法从“A<double>”转换为“A<double>”
【发布时间】:2021-08-24 23:06:25
【问题描述】:

此代码在 Visual Studio 2017 中引发编译错误:

#include <iostream>
#include <string>
using std::cin;
using std::cout;

template<class T>
class A
{
public:
    A(T a);
   ~A() {}
#if 0
    A(const A<T>&);
#else
    A(A<T>&);
#endif

    T t;
};

template<class T>
A<T>::A(T a) : t(a) {}

template <class T>
#if 0
A<T>::A(const A<T>& a) 
#else
A<T>::A(A<T>& a)
#endif
{
    t = a.t;
    std::cout << "In A copy constructor.\n";
}

int main()
{
    std::string s;
    
    A<int> a1(11);
    A<double> a2(2.71);
#if 1
    A<double> a3 = A<double>(a2);  //gives C2440 when copy constructor argument is not const.
                                   //compiler message is: 'initializing': cannot convert from 'A<double>' to 'A<double>'
#else    
    A<double> a3{a2};              //works when copy constructor argument is not const.
#endif
        
    std::cout << a3.t << "\n";
    std::cout << "Press ENTER to exit.\n";
    std::getline(std::cin, s);
}

C2440 编译失败:'initializing': cannot convert from 'A&lt;double&gt;' to 'A&lt;double&gt;'. 当前两个 #if 0s 更改为 #if 1s(选择带有 const 参数的复制构造函数)时,程序编译并运行。此外,如果所有条件编译都选择了#if 0,则程序编译并运行。

This question 没有回答我的问题。根据 cppreference.com,具有非常量参数的复制构造函数是可能的:

类T的拷贝构造函数是一个非模板构造函数,它的第一个参数是T&‍, const T&‍, volatile T&‍, or const volatile T&‍, 要么没有其他参数,要么剩下的参数都有默认值。

当我写的时候,带有非常量参数的复制构造函数仍然可以工作

A&lt;double&gt; a3{a2};

那么为什么要初始化

A&lt;double&gt; a3 = A&lt;double&gt;(a2);

当复制构造函数的参数不是 const 时不起作用?

【问题讨论】:

  • 因为不允许修改临时参数,这就是非常量引用参数的意思,“我要修改参数。”
  • 为了改进问题,请发布可以不加修改地编译的版本以给出错误(而不是发布正确的版本并描述给出错误的修改)。也没有必要包含非错误版本。
  • 注意 - 由于 C++17,A&lt;double&gt; a3 = A&lt;double&gt;(a2); 完全等同于 A&lt;double&gt; a3{a2};,但可能您使用的编译器版本不支持 C++17 或未设置为模式
  • 当我使用 C++17 编译时得到相同的结果:A&lt;double&gt; a3{a2} 有效,而当我选择不带 const 参数的复制构造函数时,A&lt;double&gt; a3 = A&lt;double&gt;(a2) 无法编译。
  • 啊,很好发现@M.M.我可以确认,使用 clang 和 gcc,当设置 -std=c++17 时,编译不会出错。

标签: c++ visual-c++ variable-assignment


【解决方案1】:

在这种情况下,特别缺少 MSVC 错误信息;如果你在上面运行 GCC,你会得到以下错误:

main.cpp: In function ‘int main()’:
main.cpp:42:20: error: cannot bind non-const lvalue reference of type ‘A<double>&’ to an rvalue of type ‘A<double>’
   42 |     A<double> a3 = A<double>(a2);  //gives C2440 when copy constructor argument is not const.
      |                    ^~~~~~~~~~~~~
main.cpp:28:15: note:   initializing argument 1 of ‘A<T>::A(A<T>&) [with T = double]’
   28 | A<T>::A(A<T>& a)
      |         ~~~~~~^
make: *** [<builtin>: main] Error 1

如果你去掉示例中所有不相关的部分,你会得到:

int main() {
  double &a = 2.71;
}

仍然返回相同的错误:

main.cpp: In function ‘int main()’:
main.cpp:2:13: error: cannot bind non-const lvalue reference of type ‘double&’ to an rvalue of type ‘double’
    2 |   double &a = 2.71;
      |             ^~~~
make: *** [<builtin>: main] Error 1

要解压,我们需要查看 C++ 中不同的 value categories 以及每个 value categories 的限制:

  • 从广义上讲,lvalue 是有名字的东西。在您原来的失败示例中,lvalue 是复制构造函数的 A&lt;T&gt; &amp;a 参数,而在我的精简示例中,它是 double &amp;a

  • rvalue 有点棘手,但实际上它是没有名字的东西。通常它被称为temporary(正如@Eljay 在他们的评论中所做的那样),因为没有名称,它没有生命周期,所以它几乎立即被破坏(如下所述的警告)。在您的示例中,rvalueA&lt;double&gt;(a2),而在我的示例中是 2.71

关于rvalues 的链接cppreference 页面有两个部分与此处相关:

  • 右值的地址不能被内置的地址操作符获取:&amp;int()&amp;i++&amp;42&amp;std::move(x)无效
  • 右值可用于初始化 const 左值引用,在这种情况下,右值标识的对象的生命周期会延长,直到引用范围结束

这第一点是导致复制构造函数出现问题的原因,而没有 const 被传递 rvalue;根据语言规则,被引用的对象在构造函数中不再有效。

第二点是允许带有const 的复制构造函数在传递rvalue 时进行操作;生命周期被延长,直到构造函数内部的引用结束,这允许构造函数复制出成员变量等。

复制消除皱纹

您初始化a3 的两种方式与第一种不同:

A<double> a3{a2};

只调用一次复制构造函数,使用lvalue (a2) 而第二次:

A<double> a3 = A<double>(a2);

调用复制构造函数两次,第一次使用上面的lvalue (a2),第二次使用rvalue(第一次构造函数调用的结果)。但是,copy elision 优化将删除其中一个调用,乍一看可能会造成混淆,因为这两个复制构造函数调用中的哪一个导致问题可能并不明显。

【讨论】:

  • A&lt;double&gt; a3 = A&lt;double&gt;(a2) 怎么会调用复制构造函数两次呢?为什么不只一次?
  • 您在A&lt;double&gt;(a2) 进行了第一次调用,在分配给a3 时,第二次用于代替自定义赋值运算符。请注意,如前所述,一个通常被优化掉;作为temporary materialization 更改的一部分,C++17 更是如此,它几乎可以保证复制省略。
  • 如果A&lt;double&gt;(a2)将左值(a2)分配给左值引用(A&lt;double&gt;&amp; a),然后赋值将左值引用(A&lt;double&gt;&amp; a)分配给左值(a3),那么在哪里您声称我将右值分配给左值引用的空间?
  • 首先,在 C++17 之前,A&lt;double&gt;(a2) 的结果是一个右值。其次,编译器将寻找 move assignment operatorcopy assignment operator 实现,如果失败,将回退到已实现的复制构造函数。因此,最终结果是尝试将右值 (A&lt;double&gt;(a2)) 绑定到非 const 左值引用 (A&lt;T&gt; &amp;a)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多