C++14 和 C++17 之间的默认构造函数调用区别

Default constructor invocation difference between C++14 and C++17

考虑以下代码(问题如下):

#include <iostream>

struct Type0
{
    Type0(char* c)
    {}
};

struct Type1
{
    Type1(int* i=nullptr) : i_(i)
    {}

    Type1(const Type1& other) = default;

    int* i_;
};

template <typename ...>
struct Composable;

template <typename T0, typename ... T>
struct Composable<T0, T...> : T0, Composable<T...>
{
    Composable()
    {
        std::cout << "Default Invoked: " << sizeof...(T) << std::endl;
    }

    Composable(const Composable& other) = default;

    template<typename Arg, typename ... Args>
    Composable(Arg&& arg, Args&& ... args) :
        T0(std::forward<Arg>(arg)), Composable<T...>(std::forward<Args>(args)...) 
    {
        std::cout << "Non-default invoked: " << sizeof...(T) << std::endl;
    }
};

template <>
struct Composable<>{};

int main()
{
    int i=1;
    char c='c';

    auto comp = Composable<Type0, Type1>(&c, &i);

    std::cout << comp.i_ << std::endl;
}

您可以找到实时代码here。这段代码有一个有趣的地方 属性:根据你是用 --std=C++17 还是 --std=C++14 选项编译它,行为会改变(你可以在我的 link 中试试这个到 live代码:编辑左下方的 g++ 调用 --std 参数)。

使用 --std=c++14,您将获得以下输出:

Non-default invoked: 0
Non-default invoked: 1
Default Invoked: 0
Non-default invoked: 1
0x0

使用 --std=C++17,你会得到这个:

Non-default invoked: 0
Non-default invoked: 1
0x7ffcdf02766c

对我来说,这种差异令人费解。很明显,C++17 版本做的是正确的,而 C++14 是错误的。 C++14 版本正在调用 Composable 和(从中)Type1 的默认构造函数(这是 0x0 最后一行输出的来源,因为 Type1 将此作为其 i 构造函数参数的默认值)。但是,我没有看到应该调用默认构造函数的任何地方。

此外,如果我完全注释掉 Composable 的默认构造函数,C++17 版本与以前完全相同,而 C++14 版本现在无法编译,抱怨缺少默认构造函数。如果有任何希望通过不同的优化行为以某种方式解释差异,那么这个事实肯定会扼杀它(无论如何希望很小,因为观察到的差异在所有优化级别(包括 0)中都存在)。

谁能解释一下这个区别? C++14 的行为是错误,还是我不理解的某些预期行为?如果 C++14 的行为在 C++14 的规则内是正确的,有人可以解释默认构造函数调用的来源吗?

保证复制省略。

这一行:

auto comp = Composable<Type0, Type1>(&c, &i);

在 C++17 中,这与以下内容完全相同:

Composable<Type0, Type1> comp(&c, &i);

如果您更改到此版本,您将看到 C++14 和 C++17 之间的相同行为。然而,在 C++14 中,这仍然是一个移动构造(或者,正如您稍后将看到的更技术上正确的,复制初始化)。但是在 Composable 中,您没有隐式生成的移动构造函数,因为您有一个用户声明的复制构造函数。因此,对于移动构造,您的 "Non-default invoked" 构造函数模板在 C++14 版本中被调用(它比复制构造函数更匹配):

template<typename Arg, typename ... Args>
Composable(Arg&& arg, Args&& ... args) :
    T0(std::forward<Arg>(arg)), Composable<T...>(std::forward<Args>(args)...) 

这里,ArgComposable<Type0, Type1>Args是空包。我们委托给 T0 (Type0) 的构造函数,转发整个 Composable (这是有效的,因为它公开继承自 Type0,所以我们得到隐式生成的移动构造函数那里)和 Composable<Type1> 的默认构造函数(因为 args 是空的)。

此构造函数模板实际上 不是一个正确的移动构造函数 - 它根本不会初始化 Type1 成员。您不是从右侧的 Type1::i_ 移动,而是调用默认构造函数 Type1::Type1(),这就是为什么您最终得到 0.

如果添加适当的移动构造函数:

Composable(Composable&& other) = default;

然后你会再次看到 C++14 和 C++17 之间的相同行为。