【问题标题】:C++ copy constructor causing code not to compile ( gcc )C++ 复制构造函数导致代码无法编译( gcc )
【发布时间】:2009-10-04 23:23:24
【问题描述】:

我有以下无法编译的代码。编译器错误是:

"error: no matching function to call B::B(B)",
candidates are B::B(B&) B::B(int)"

代码在以下两种情况下编译:

  1. 取消注释函数 B(const B&)
  2. 把'main'改成下面的

    int main()
    {
            A a;
            B b0;
            B b1 = b0;
            return 0;
    }
    

如果我执行 1,代码会编译,但从输出中它说它正在调用“非 const 复制构造函数”。

谁能告诉我这里发生了什么?

using namespace std;
class B
{
    public:
    int k;
     B()
     { 
        cout<<"B()"<<endl; 
     }
     B(int k) 
     { 
        cout<<"B(int)"<<endl;
        this->k = k;
     }
     /*B(const B& rhs) { 
        cout<<"Copy constructor"<<endl;
        k = rhs.k;
     }*/
     B(B& rhs) 
     { 
        cout<<"non const Copy constructor"<<endl;
        k = rhs.k;
     }
     B operator=(B& rhs)
     {
        cout<<"assign operator"<<endl;
        k = rhs.k;
        return *this;
     }  
};

class A
{
    public:
        B get_a(void)
        {
            B* a = new B(10);
            return *a;
        }       
};

int main()
{
    A a;
    B b0 = a.get_a();  // was a.just();
    B b1 = b0 ;
    return 0;
}

【问题讨论】:

  • 对不起。它应该是 a.get_a()

标签: c++ constructor reference copy constants


【解决方案1】:

我对此进行了一些额外的阅读,并且正如我一直怀疑的那样,发生这种情况的原因是由于返回值优化。正如Wikipedia article 解释的那样,RVO 是允许编译器在分配临时对象或将它们复制到永久变量的过程中消除临时对象的允许机制。此外,RVO 是允许违反 as-if 规则的少数功能之一(如果不是唯一的话),即只有当编译器具有相同的可观察行为时,才允许编译器进行优化 好像优化从来没有放在首位——这是解释这里行为的关键(我承认我只是在研究这个问题时才知道这个豁免,这就是为什么我也是一开始很困惑)。

在您的情况下,GCC 足够聪明,可以避免两个副本之一。将您的代码简化为一个更简单的示例

B returnB()
{
    B a;
    B* b = &a;
    return *b;
}

int main()
{
    B c = returnB();
    return 0;
}

如果一个符合标准,不做RVO,在制作c的过程中会做两份——*b的拷贝成returnB的返回值,返回值的拷贝进入c 本身。在您的情况下,GCC 省略了第一个副本,而是只制作一个副本,从 *b 直接复制到 c。这也解释了为什么调用B(B&amp;) 而不是B(const B&amp;)——因为*b(又名a)不是临时值,编译器不再需要使用B(const B&amp;),而是选择更简单的@在构造 c 时调用 987654336@ 代替(如果存在选择,非const 重载总是自动优先于const 重载)。

那么,如果B(const B&amp;) 不存在,为什么编译器仍然会报错?这是因为您的代码的语法必须正确,然后才能进行优化(如 RVO)。在上面的例子中,returnB()返回一个临时的(根据 C++ 语法规则),所以编译器必须看到一个B(const B&amp;) 复制构造函数。但是,一旦编译器确认您的代码在语法上是正确的,它就可以进行优化,以使 B(const B&amp;) 无论如何都不会被使用。

编辑:向在 C++ 标准中发现以下内容的 Charles Bailey 致敬

12.2 [class.temporary]:“即使避免创建临时对象, 所有的语义限制必须是 像临时对象一样受到尊重 已创建。”

当要复制临时对象以进行构造时(无论是否实际使用构造函数),这只是加强并确认需要一个复制构造函数来引用 const

【讨论】:

  • 很好地解释了 RVO。 FWIW,这是我发现有用的 RVO 的另一种解释:parashift.com/c++-faq-lite/ctors.html#faq-10.9
  • 感谢您的链接。事实上,那些有兴趣了解更多关于 RVO 的人可能应该阅读这样的文章,因为我的回答并不是真的要笼统地解释 RVO,而只是在这个特定的上下文中。
  • 很好的答案,但是您对“RVO”的使用非常不正统,并且有关该主题的维基百科文章是完全错误的。您所描述的是命名返回值优化,简称“NRVO”。实际上,Wikipedia 文章包含一个特定情况,即只有 NRVO 失败。严格来说,可能没有 RVO 这样的东西——尽管它可以被定义为返回值被转换为输出参数的简单事实。见msdn.microsoft.com/en-us/library/ms364057%28VS.80%29.aspx
  • 嗯……维基百科的文章正确地引用了迈耶斯(刚刚检查过),我想这算是权威。计算机科学再一次证明了它完全无法就定义明确的术语达成一致。
  • 在问题中的函数中创建的副本(可能会被 NRVO 消除)可以与任何一种形式的复制构造函数一起使用。实际上,在这种情况下,在被调用函数之外为返回值创建的临时值更为重要。这个临时性也可以被消除,但是消除这个临时性与NRVO是否发生在函数内部无关。
【解决方案2】:

您的get_a() 函数返回一个对象B,而不是引用(但会泄漏新创建的B 对象)。此外,要让赋值按您的方式工作,您需要确保您的赋值运算符和复制构造函数都采用const B&amp; 参数——然后B b1 = b0 将在这种情况下工作。这有效:

class B
{
public:
  int k;
  B() { cout<<"B()"<<endl; }
  B(int k) { 
    cout<<"B(int)"<<endl;
    this->k = k;
  }
  B(const B& rhs) { 
    cout<<"non const Copy constructor"<<endl;
    k = rhs.k;
  }
  B operator=(const B& rhs) {
    cout<<"assign operator"<<endl;
    k = rhs.k;
    return *this;
  }
};

class A {
public:
  B* get_a(void) {
    B* a = new B(10);
    return a;
  }
  B get_a2(void) {
    B a(10);
    return a;
  }
};

int main() {
  A a;
  B b0 = *a.get_a(); // bad: result from get_a() never freed!
  B b1 = a.get_a2(); // this works too
  return 0;
}

【讨论】:

  • 没关系。我并没有真正为此烦恼。我只是想知道为什么它不会编译
  • 是的,当您在复制构造函数中将引用设为“const”时,它会编译,我之前已经注意到,我的问题是为什么?
  • C++ 将通过 const 引用为接受对象的函数传递对象。即这就是为什么printB(const B&amp; obj) 可以称为printB(b1)
  • 我认为没有人回答“为什么?”所以我试一试:右值(又名临时变量,例如函数返回的值)不能绑定到非常量引用,所以为了让函数/构造函数能够将右值作为参数绑定到引用类型,它必须是一个常量引用。
  • @GRB,感谢您的解释。如果我取消注释接受常量引用的复制构造函数,代码就会编译。但是,当您运行程序时,它使用的是使用非常量引用的复制构造函数。我正在使用 gcc-4.3。
【解决方案3】:

简短说明:按值返回的函数会创建一个临时对象,该对象被视为常量,因此不能传递给通过引用接受的函数,它只能传递给接受 const 引用的函数。如果你真的在 get_a() 中分配了一个对象,你真的应该返回一个指针(以便你记得删除它,希望如此)或者在最坏的情况下 - 一个引用。如果你真的想返回一个副本 - 在堆栈上创建对象。

长解释:要理解为什么只有“非 const 复制构造函数”1 时您的代码无法编译,您需要熟悉术语 lvalue右值。他们原本的意思是 rvalues 可以only 站在 operator = (assignment) 的右侧,而 lvalues 也可以站在左侧.示例:

T a, b;
const T ac;
a = b; // a can appear on the left of = 
b = a; // so can b => a and b are lvalues in this context
ac = a; // however, ac is const so assignment doesn't make sense<sup>2</sup>, ac is a rvalue

当编译器执行重载解析(查找函数/方法的哪个重载最匹配提供的参数)时,它将允许 lvalues 匹配由 value 传递的参数3、引用和常量引用类型。但是,它只会匹配 rvalues 与 value3 和 const 引用参数。这是因为在某种意义上,由于rvalues不能放在operator =的左侧,它们具有只读语义,并且不应该被允许修改它们。当你通过非常量引用接受一个参数时,这意味着你会以某种方式改变这个参数。

最后一块拼图:临时对象是右值。按值返回的函数会创建一个寿命非常有限的临时对象。由于它的生命周期有限,它被认为是 const,因此是一个 rvalue。而且这个 rvalue 不匹配带有非常量引用参数的函数。例子:

void f_cref(const A& a) { std::cout << "const ref" << std::endl; }
void f_ref(A& a) { std::cout << "non-const ref" << std::endl; }

A geta() { return A(); }

A a;
const A ac;
f_ref(a); // ok, a is a lvalue
f_ref(ac); // error, passing const to non-const - rvalue as lvalue - it's easy to spot here
f_cref(a); // ok, you can always pass non-const to const (lvalues to rvalues)
f_ref(geta()); // error, passing temporary and therefore const object as reference
f_cref(geta()); // ok, temporary as const reference

现在您已经掌握了所有信息,可以找出代码无法编译的原因。复制构造函数就像普通函数一样。

我把事情简化了一点,所以更好、更完整、更正确的解释可以在这个优秀的Visual C++ Studio Team blog post about rvalue references 中找到,它还解决了新的 C++ 0x 特性“右值引用”

1 - 没有非常量复制构造函数之类的东西。复制构造函数接受 const 引用,句点。

2 - 你可以将 const 对象放在 = 的左边,如果它的 operator = 声明为 const。但这将是可怕的,可怕的,荒谬的事情。

3 - 实际上,如果 A 没有复制构造函数(即接受 const A& 的复制构造函数),您将无法按值传递 const A。

【讨论】:

  • rvalue/lvalue 和 const/non-const 是正交的。当您说右值被认为是 const 时,严格来说并非如此。考虑函数A tmp()。您可以直接在返回值上调用非常量函数:tmp().non_const_method()。您不能将返回值绑定到非 const 引用,因为它不是左值,与它的 const 性无关(它不是 const)。
【解决方案4】:

B b1 = b0; 是罪魁祸首。此行需要调用复制构造函数。你可以改写B b1(b0) 来修复代码,或者定义一个复制构造函数,它接受const B&amp;,而不是B&amp;

【讨论】:

  • B b1 = b0; 是初始化,而不是赋值,因此它总是使用构造函数。 B::operator= 的存在与否与这行代码的语义无关。
  • 我已经删除了我的 -1,但还有一个小问题:复制构造函数不能按值获取 B,它必须获取一些限定的引用。如果通过值调用 B,则调用复制构造函数将是无限递归的。
猜你喜欢
  • 2021-11-25
  • 2013-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-20
  • 1970-01-01
相关资源
最近更新 更多