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

时间:2018-12-04 17:37:09

标签: c++ templates c++14 c++17 variadic-templates

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

#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选项进行编译,行为会发生变化(您可以在我的实时代码链接中尝试以下操作:编辑g ++调用{{ 1}}参数)。

使用--std,您将获得以下输出:

--std=c++14

使用Non-default invoked: 0 Non-default invoked: 1 Default Invoked: 0 Non-default invoked: 1 0x0 ,您会得到以下信息:

--std=C++17

对我来说,这种区别令人困惑。显然C ++ 17版本做正确的事,而C ++ 14是错误的。 C ++ 14版本正在调用Non-default invoked: 0 Non-default invoked: 1 0x7ffcdf02766c Composable的默认构造函数(这是Type1的最后一行来自的地方,因为{{1} }将此作为其0x0构造函数参数的默认值)。但是,我看不到应该调用默认构造函数的任何地方。

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

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

1 个答案:

答案 0 :(得分:7)

保证复制省略。

此行:

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

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

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

如果更改为该版本,则在C ++ 14和C ++ 17之间将看到相同的行为。但是,在C ++ 14中,这仍然是一个移动构造(或者,从技术上讲,如稍后所看到的,它是复制初始化)。但是在Composable中,您没有隐式生成的move构造函数,因为您有一个用户声明的副本构造函数。结果,对于move构造,您的“非默认调用”构造函数模板在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是一个空包。我们将委派给T0Type0)的构造函数,转发整个Composable(之所以起作用,是因为它公开地继承自Type0,所以我们得到了隐式生成的move构造函数)和Composable<Type1>的默认构造函数(因为args为空)。

此构造函数模板不是真正的 正确的move构造函数-它根本不会初始化Type1成员。而不是从右侧的Type1::i_移动,而是调用默认构造函数Type1::Type1(),这就是为什么最终使用0的原因。

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

Composable(Composable&& other) = default;

然后,您将再次在C ++ 14和C ++ 17之间看到相同的行为。