这段代码在 C++17 中编译失败了吗?

49

我正在更新一个项目,让它使用C++17,并发现一些遵循这种模式的代码在最近版本的clang上会导致编译错误:

#include <boost/variant.hpp>

struct vis : public boost::static_visitor<void>
{
    void operator()(int) const { }
};

int main()
{
    boost::variant<int> v = 0;
    boost::apply_visitor(vis{}, v);
}

在 C++17 模式下使用 clang v8.0,会导致以下错误

<source>:11:30: error: temporary of type 'boost::static_visitor<void>' has protected destructor
    boost::apply_visitor(vis{}, v);
                             ^
/opt/compiler-explorer/libs/boost_1_64_0/boost/variant/static_visitor.hpp:53:5: note: declared protected here
    ~static_visitor() = default;

然而,在C++14模式下可以编译。我发现如果我将大括号初始化vis{}更改为小括号vis(),则两种模式都可以正确编译。我尝试的每个gcc版本都允许在C++17模式下使用这两种变体。
这是从C++14到C++17行为上的正确变化,还是clang的一个错误?如果是正确的,为什么在C++17中现在无效(或者可能一直无效,但clang只允许在早期标准修订版中使用)?

7
在1.70版本中增强[fixed](https://github.com/boostorg/variant/commit/dd728220b00772cee5d3aea5850a2bb09eecc66b#diff-e8948f8833a40a3c873b7f8d4ff5f9d7)。这也在此处(https://reviews.llvm.org/D53860)讨论过。 - interjay
1个回答

63

clang 是正确的。这里是一个简化的例子:

struct B {
protected:
    B() { }
};

struct D : B { };

auto d = D{};

在C++14中,D不是一个聚合体,因为它有一个基类,所以D{}是“正常的”(非聚合的)初始化,它调用D的默认构造函数,进而调用B的默认构造函数。这没问题,因为D可以访问B的默认构造函数。
在C++17中,聚合的定义被扩大了 - 基类现在是允许的(只要它们是非虚拟的)。D现在是一个聚合体,这意味着D{}是聚合初始化。在聚合初始化中,这意味着我们(调用者)正在初始化所有子对象 - 包括基类子对象。但是我们没有访问B的构造函数(它是受保护的),所以我们不能调用它,这是不合法的。
别担心,修复很简单。使用括号:
auto d = D();

这与之前调用 D 的默认构造函数有关。

7
太好了,这正是我在寻找的:扩大了“聚合物”的定义是罪魁祸首。 - Jason R
clang 是否正确?子对象初始化程序的可访问性上下文是什么?关于这个问题有一个核心语言问题:issue#2244。作为一名程序员,我希望在两种情况下,即聚合上下文中对dcl.init.aggr/5.1dcl.init.aggr/5.2的名称可访问性检查已完成。 - Oliv
2
我听说在C++20中,括号和大括号被统一了。这会影响修复的有效性吗? - L. F.
2
稍有偏差的缩减。在C++17中的问题是dtor(static_visitor本身是C++17聚合,因此ctor的保护性并不重要)。在C++20中,两者都存在问题,而boost修复程序(仅修复了dtor)将再次出现故障,这使得它成为C++20 OMG-这些人为的例子是如此令人惊讶,我们必须解决它的变化破坏事物的一个很好的例子。 - T.C.
@T.C. 哦,我现在明白你的意思了。我想这是一个略微不同的问题,但根本原因相同。 - Barry
显示剩余4条评论

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