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)...)
这里,Arg
是Composable<Type0, Type1>
,Args
是空包。我们委托给 T0
(Type0
) 的构造函数,转发整个 Composable
(这是有效的,因为它公开继承自 Type0
,所以我们得到隐式生成的移动构造函数那里)和 Composable<Type1>
的默认构造函数(因为 args
是空的)。
此构造函数模板实际上 不是一个正确的移动构造函数 - 它根本不会初始化 Type1
成员。您不是从右侧的 Type1::i_
移动,而是调用默认构造函数 Type1::Type1()
,这就是为什么您最终得到 0
.
如果添加适当的移动构造函数:
Composable(Composable&& other) = default;
然后你会再次看到 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;
}
您可以找到实时代码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)...)
这里,Arg
是Composable<Type0, Type1>
,Args
是空包。我们委托给 T0
(Type0
) 的构造函数,转发整个 Composable
(这是有效的,因为它公开继承自 Type0
,所以我们得到隐式生成的移动构造函数那里)和 Composable<Type1>
的默认构造函数(因为 args
是空的)。
此构造函数模板实际上 不是一个正确的移动构造函数 - 它根本不会初始化 Type1
成员。您不是从右侧的 Type1::i_
移动,而是调用默认构造函数 Type1::Type1()
,这就是为什么您最终得到 0
.
如果添加适当的移动构造函数:
Composable(Composable&& other) = default;
然后你会再次看到 C++14 和 C++17 之间的相同行为。