首页 > 解决方案 > C++14和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;
}

您可以在此处找到实时代码。此代码有一个有趣的属性:根据您是否使用--std=C++17or--std=C++14选项编译它,行为会发生变化(您可以在我的实时代码链接中尝试此操作:编辑--std左下角的 g++ 调用参数)。

使用--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它提供了 this 作为其i构造函数参数的默认值)。但是,我没有看到应该调用默认构造函数的任何地方。

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

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

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

解决方案


保证复制省略。

这一行:

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

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

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

如果您更改到此版本,您将在 C++14 和 C++17 之间看到相同的行为。但是,在 C++14 中,这仍然是一个移动构造(或者,在技术上更正确,正如您稍后会看到的,复制初始化)。但是在 中Composable,您没有隐式生成的移动构造函数,因为您有一个用户声明的复制构造函数。因此,对于移动构造,您的“非默认调用”构造函数模板在 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 之间的相同行为。


推荐阅读