为什么在构造函数初始化列表中使用C++11花括号初始化方式无法工作,而括号初始化方式可以?

12

当在构造函数初始化列表中初始化引用抽象类型时,如何对{}初始化与()初始化进行区分?请看下面的Bar类:

class AbstractBase
{
public:
    AbstractBase() {}
    virtual ~AbstractBase() = default;

    virtual void ab() = 0;
};

class Foo : public AbstractBase
{
public:
    Foo() {}

    void ab() {}
};

class Bar
{
public:
    Bar(const AbstractBase& base) : myBase{base} {}

private:
    const AbstractBase& myBase;
};


int main()
{
    Foo f{};
    Bar b{f};

}

编译时出现错误

test5.cpp: In constructor ‘Bar::Bar(const AbstractBase&)’:
test5.cpp:22:48: error: cannot allocate an object of abstract type ‘AbstractBase’
     Bar(const AbstractBase& base) : myBase{base}
                                                ^
test5.cpp:2:7: note:   because the following virtual functions are pure within ‘AbstractBase’:
 class AbstractBase
       ^
test5.cpp:8:18: note:   virtual void AbstractBase::ab()
     virtual void ab() = 0;

更改该行

Bar(const AbstractBase& base) : myBase(base) {}

编译和运行都很好。

阅读Stroustrup的C++11书籍时,我认为在大多数情况下,{}与()是相同的,除了存在std::initializer_list<>构造函数和其他构造函数之间存在歧义以及使用auto作为类型的情况,但这里我都不涉及。


1
我的经验法则是:对于元素列表(包括零个元素),使用 {},而对于其他构造函数,则使用 () 进行显式调用。 - Mooing Duck
1
这实际上是与引用列表初始化有关的问题。 - M.M
与此处相同的问题 - https://dev59.com/DHfZa4cB1Zd3GeqPXu2G - Rudolfs Bundulis
@RudolfsBundulis 谢谢你的发现,我一直在想为什么没有 S() {} 就连编译都过不了我的代码示例! - M.M
@MattMcNabb 是的,从你的回答中我明白了区别,我没有深入思考。 - Rudolfs Bundulis
同一问题的更接近版本:https://dev59.com/XoHba4cB1Zd3GeqPTZkb#24794857 - M.M
1个回答

13
简短回答: 这是标准中的一个错误,在C++14中已经修复,g++ 4.9有修复(也适用于C++11模式)。缺陷报告1288

这里有一个更简单的例子:

struct S
{
    int x;
    S() { }     // this causes S to not be an aggregate (otherwise aggregate 
                // initialization is used instead of list initialization)
};

S x = 5;
S const &y { x } ;    

x = 6;
std::cout << y << std::endl;     // output : 5

在C++11中,S const &y {x}的意义并不是将y绑定到x;实际上,其含义是创建一个临时变量,并将引用绑定到该临时变量。根据C++11 [dcl.init.ref]/3:

否则,如果T是引用类型,则列表初始化一个T所引用类型的prvalue临时变量,并将引用绑定到该临时变量。[注:与往常一样,如果引用类型是对非const类型的左值引用,则绑定将失败,程序将是有缺陷的。 -注释 ]

这很愚蠢,显然这段代码的意图是直接将y绑定到x。在C++14中,文本已经更改:

否则,如果初始化列表具有单个类型为E的元素,并且T不是引用类型或其引用类型与E相关,则从该元素初始化对象或引用;

由于类型与自身(或其基类之一)相关联,因此在此示例中和您的实际代码中,它应该正确地绑定。
编译器遵循C++11措辞并尝试从base创建临时变量以绑定引用,因此出现错误消息;这是因为base属于抽象类型。

如果我在使用{}初始化时从我的引用中删除const,那么我会得到不同的错误: test5.cpp: 在构造函数‘Bar::Bar(AbstractBase&)’中: test5.cpp:22:42: 错误:无效的非const引用初始化类型‘AbstractBase&’,来自类型为‘<brace-enclosed initializer list>’的rvalue Bar(AbstractBase& base) : myBase{base} {} ^ - oryan_dunn
3
@oryan_dunn试图将一个临时对象绑定到一个非const引用上。 - M.M
是的,我现在意识到了,它试图创建一个抽象类型的临时对象。 我想在C++14出现之前,我将不得不继续在所有构造函数初始化列表上使用()以保持一致。 - oryan_dunn
不,你不需要等待C++14,你可以使用实现DR1288的更新编译器。 - Jonathan Wakely
我看到GCC 4.9可以解决这个问题,但我被RHEL 6.5和devtoolset 2.1困在了4.8.2上。 - oryan_dunn

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