隐式转换和复制构造函数

4

更新:建议的重复问题只解决了这个问题的一部分。理解正在发生的事情(首先创建临时引用)的关键在那里没有得到解释。

这是我第一次遇到隐式转换,所以我写了这个:

class A {};

class B {
public:
    B(A& other) {}
    // Copy constructor
    B(const B& other) {}
};

int main() {
    A foo;
    B bar = foo;
}

这段代码编译时没有问题,但如果我去掉const关键字,我的编译器(gcc 版本 4.8.4)会在赋值处报错,错误信息让我无法理解:

test.cc: In function ‘int main()’:
test.cc:12:13: error: no matching function for call to ‘B::B(B)’
     B bar = foo;
             ^
test.cc:12:13: note: candidates are:
test.cc:7:5: note: B::B(B&)
     B(B& other) {}
     ^
test.cc:7:5: note:   no known conversion for argument 1 from ‘B’ to ‘B&’
test.cc:5:5: note: B::B(A&)
     B(A& other) {}
     ^
test.cc:5:5: note:   no known conversion for argument 1 from ‘B’ to ‘A&’

这是有效的C++代码吗?为什么当我一开始尝试将 A 赋值时,它会显示 no matching function for call to ‘B::B(B)’


这一定与移动运算符有关,因为如果删除const,则B bar(foo);可以正常工作而不会出现任何问题。 - jpo38
有趣的是,在gcc 4.1.2下删除const也能正常工作。 - dbush
@dbush 不确定编译器在优化时消除复制构造函数调用时是否必须提供诊断信息,但语言要求其可访问性。 - Slava
2个回答

7
这个声明
B bar = foo;

以下是工作原理:

首先,编译器使用构造函数创建一个临时对象:

B(A& other) {}

然后它尝试在复制构造函数中使用此临时对象:

B(B& other) {}

但是它可能无法将临时对象与非常量引用捆绑在一起,这将导致错误。

当你使用等号时,就会使用所谓的复制初始化。

如果你写了:

B bar( foo );

那么这里将使用所谓的直接初始化,即不调用复制构造函数。在这种情况下,此代码将编译。

请注意,复制/移动构造函数可能会被绕过,并且临时对象可以直接构建在目标对象中。这称为复制省略。尽管如此,所有规则都应该像显式调用复制/移动构造函数一样遵守。

例如,如果您为类B的构造函数添加输出语句

class A {};

class B {
public:
    B(A& other) { std::cout << "B::B( A & )" << std::endl; }
    // Copy constructor
    B(const B& other) { std::cout << "B::B( const B & )" << std::endl; }
};

int main()
{
    A foo;
    B bar = foo;
}

如果没有出现消息,您将看不到该消息。
B::B( const B & )

然而,复制构造函数应该是可访问的。
例如,如果您将其设置为私有。
class A {};

class B {
public:
    B(A& other) { std::cout << "B::B( A & )" << std::endl; }
    // Copy constructor
private:
    B(const B& other) { std::cout << "B::B( const B & )" << std::endl; }
};

int main()
{
    A foo;
    B bar = foo;
}

该程序无法编译(仅限于非MS VC++编译器。:))

我认为你应该提到复制构造函数可能并不会被调用,语义要求它仍然可访问。 - Slava
@Slava 我追加了我的帖子。 - Vlad from Moscow
很棒的答案!不过我还是选择接受@0x499602D2的回答,因为对我来说更加清晰易懂。 - Alba Mendez

5

B bar = foo 被称为 拷贝初始化(copy-initialization): 当右侧的类型与左侧的类型不匹配时(在本例中,一个 B 和一个 A),编译器会创建一个从右侧初始化的临时对象,然后使用拷贝构造函数进行复制。这实际上就是你的代码:

B bar = B(foo);

由于您从复制构造函数中删除了 const,因此现在会出现错误,因为您试图将 rvalue 绑定到 lvalue 引用上。正如您已经看到的那样,rvalue 可以绑定到 const lvalue 引用。

您可以通过使用 直接初始化 来解决这个问题。现在不再创建任何副本:

B bar(foo);

附注:正是这篇cplusplus.com的教程(请参见第二节中的示例)让我想到了直接调用转换构造函数。 - Alba Mendez
1
@jmendeth确实,cplusplus不是最好的教程网站。我也看到它在关于允许转换的“三个”成员函数方面存在误解。实际上只有两个,一个转换构造函数(单参数构造函数)和一个转换运算符。赋值运算符只是另一个成员函数,与转换无关。 - David G

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接