C++构造函数:完美的转发和重载
C++ Constructor: Perfect forwarding and overload
我有两个类,A和B,B派生自A。
A 有多个构造函数(下例中有 2 个)。
B 还有一个要初始化的成员(它有一个默认初始化器)。
我怎样才能实现 B 可以使用 A 的构造函数之一来构造,而不必在 B 中手动重写 A 的所有构造函数重载?
(在下面的示例中,否则我必须为 B 提供四个构造函数:B():A(){}
、B(string s):A(s){}
、B(int b):A(),p(b){}
、B(string s, int b):A(s),p(b){}
,而不是两个,至少在忽略默认参数的可能性时)。
我的方法是完美转发,但是下面的场景会报错:
#include <utility>
#include <string>
struct A {
A(const std::string& a) : name(a) {}
A(){}
virtual ~A(){}
std::string name;
};
struct B : public A {
template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}
B(const std::string& a, int b) : A(a), p(b) {}
int p = 0;
};
int main()
{
B b1("foo");
B b2("foobar", 1);
}
对于 b2,GCC 抱怨 no matching function for call to 'A::A(const char [5], int)
。
显然它试图调用完美的转发构造函数,这显然不应该工作,而不是 B 的第二个构造函数。
为什么编译器看不到第二个构造函数而是调用它?是否存在编译器无法找到 B 的正确构造函数的技术原因?
我该如何解决这个问题?
确切的错误信息:
main.cpp: In instantiation of 'B::B(Args&& ...) [with Args = {const char (&)[5], int}]':
main.cpp:26:19: required from here
main.cpp:15:54: error: no matching function for call to 'A::A(const char [5], int)'
B(Args&&... args) : A(std::forward<Args>(args)...) {}
^
main.cpp:6:5: note: candidate: A::A()
A(){}
^
main.cpp:6:5: note: candidate expects 0 arguments, 2 provided
main.cpp:5:5: note: candidate: A::A(const string&)
A(const std::string& a) : name(a) {}
^
main.cpp:5:5: note: candidate expects 1 argument, 2 provided
main.cpp:4:8: note: candidate: A::A(const A&)
struct A {
^
main.cpp:4:8: note: candidate expects 1 argument, 2 provided
"foobar"
是一个 const char (&) [7]
。因此 Args
比 const std::string&
更匹配
因此,选择了这个重载:
template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}
其中 Args
是 const char (&) [7]
所以它变成:
B(const char (&&args_0) [7], int&& args_1)
它被转发给 A
的 2 参数构造函数...它不存在。
The wanted behavior is that if you construct a B with a constructor that works for A then the "...Args constructor" of B is called, otherwise another constructor of B gets called, otherwise it fails with "no appropriate constructor for B found".
像这样...
#include <utility>
#include <string>
struct A {
A(std::string a) : name(std::move(a)) {}
A(){}
virtual ~A(){}
std::string name;
};
template<class...T> struct can_construct_A
{
template<class...Args> static auto test(Args&&...args)
-> decltype(A(std::declval<Args>()...), void(), std::true_type());
template<class...Args> static auto test(...) -> std::false_type;
using type = decltype(test(std::declval<T>()...));
static constexpr bool value = decltype(test(std::declval<T>()...))::value;
};
struct B : public A {
template<class...Args>
B(std::true_type a_constructable, Args&&...args)
: A(std::forward<Args>(args)...)
{}
template<class Arg1, class Arg2>
B(std::false_type a_constructable, Arg1&& a1, Arg2&& a2)
: A(std::forward<Arg1>(a1))
, p(std::forward<Arg2>(a2))
{
}
template<typename... Args>
B(Args&&... args)
: B(typename can_construct_A<Args&&...>::type()
, std::forward<Args>(args)...) {}
int p = 0;
};
int main()
{
B b1("foo");
B b2("foobar", 1);
}
After seeing that A doesn't have a matching constructor, why doesn't it go back and continue looking for other constructors of B that might match? Are there technical reasons?
简而言之(并且非常简单),当发生重载解析时,编译器会执行以下操作:
展开所有可能匹配给定参数的模板化重载。将它们添加到列表中(权重表示到达那里所涉及的专业化水平)。
将任何具体的重载添加到列表中,这些重载可以通过合法地将转换运算符应用于参数来实现,权重表示将提供的参数转换为函数参数类型需要多少次转换.
按升序 'work' 或权重对列表进行排序。
选择需要最少工作的一项。如果有一个最好的平局,错误。
编译器在这方面做得很好。这不是递归搜索。
我提前向我们中间的纯粹主义者道歉,他们会觉得这种幼稚的解释令人反感:-)
无需赘述,转发构造函数几乎总是首选。它甚至可能比复制构造函数更受欢迎。
避免这种歧义的一种技术是让调用者明确select他们是否需要转发构造函数,通过使用一个虚拟参数:
struct B : A
{
enum dummy_t { forwarding };
// ...
template<typename... Args>
B(dummy_t, Args&&... args) : A(std::forward<Args>(args)...) {}
};
使用示例:
B b2("foobar", 1);
B b(B::forwarding, "foobar");
那么你甚至可以有一个 A
和一个 B
具有相同参数的构造函数。
您的问题的另一种解决方案是在 B
的定义中写入 using A::A;
。这就像为 B
提供一组匹配 A
的构造函数,它们通过使用相同的参数调用相应的 A
构造函数来初始化 A
。
当然这有一些缺点,例如您不能同时初始化其他 B
成员。 Further reading
选项 #1
从 class A
:
继承构造函数
struct B : A
{
using A::A;
// ~~~~~~~~~^
B(const std::string& a, int b) : A(a), p(b) {}
int p = 0;
};
选项 #2
使 B 的可变参数构造函数 SFINAE 可用:
#include <utility>
struct B : A
{
template <typename... Args, typename = decltype(A(std::declval<Args>()...))>
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
B(Args&&... args) : A(std::forward<Args>(args)...) {}
B(const std::string& a, int b) : A(a), p(b) {}
B(B& b) : B(static_cast<const B&>(b)) {}
B(const B& b) : A(b) {}
int p = 0;
};
我有两个类,A和B,B派生自A。 A 有多个构造函数(下例中有 2 个)。 B 还有一个要初始化的成员(它有一个默认初始化器)。
我怎样才能实现 B 可以使用 A 的构造函数之一来构造,而不必在 B 中手动重写 A 的所有构造函数重载?
(在下面的示例中,否则我必须为 B 提供四个构造函数:B():A(){}
、B(string s):A(s){}
、B(int b):A(),p(b){}
、B(string s, int b):A(s),p(b){}
,而不是两个,至少在忽略默认参数的可能性时)。
我的方法是完美转发,但是下面的场景会报错:
#include <utility>
#include <string>
struct A {
A(const std::string& a) : name(a) {}
A(){}
virtual ~A(){}
std::string name;
};
struct B : public A {
template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}
B(const std::string& a, int b) : A(a), p(b) {}
int p = 0;
};
int main()
{
B b1("foo");
B b2("foobar", 1);
}
对于 b2,GCC 抱怨 no matching function for call to 'A::A(const char [5], int)
。
显然它试图调用完美的转发构造函数,这显然不应该工作,而不是 B 的第二个构造函数。
为什么编译器看不到第二个构造函数而是调用它?是否存在编译器无法找到 B 的正确构造函数的技术原因? 我该如何解决这个问题?
确切的错误信息:
main.cpp: In instantiation of 'B::B(Args&& ...) [with Args = {const char (&)[5], int}]':
main.cpp:26:19: required from here
main.cpp:15:54: error: no matching function for call to 'A::A(const char [5], int)'
B(Args&&... args) : A(std::forward<Args>(args)...) {}
^
main.cpp:6:5: note: candidate: A::A()
A(){}
^
main.cpp:6:5: note: candidate expects 0 arguments, 2 provided
main.cpp:5:5: note: candidate: A::A(const string&)
A(const std::string& a) : name(a) {}
^
main.cpp:5:5: note: candidate expects 1 argument, 2 provided
main.cpp:4:8: note: candidate: A::A(const A&)
struct A {
^
main.cpp:4:8: note: candidate expects 1 argument, 2 provided
"foobar"
是一个 const char (&) [7]
。因此 Args
比 const std::string&
因此,选择了这个重载:
template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}
其中 Args
是 const char (&) [7]
所以它变成:
B(const char (&&args_0) [7], int&& args_1)
它被转发给 A
的 2 参数构造函数...它不存在。
The wanted behavior is that if you construct a B with a constructor that works for A then the "...Args constructor" of B is called, otherwise another constructor of B gets called, otherwise it fails with "no appropriate constructor for B found".
像这样...
#include <utility>
#include <string>
struct A {
A(std::string a) : name(std::move(a)) {}
A(){}
virtual ~A(){}
std::string name;
};
template<class...T> struct can_construct_A
{
template<class...Args> static auto test(Args&&...args)
-> decltype(A(std::declval<Args>()...), void(), std::true_type());
template<class...Args> static auto test(...) -> std::false_type;
using type = decltype(test(std::declval<T>()...));
static constexpr bool value = decltype(test(std::declval<T>()...))::value;
};
struct B : public A {
template<class...Args>
B(std::true_type a_constructable, Args&&...args)
: A(std::forward<Args>(args)...)
{}
template<class Arg1, class Arg2>
B(std::false_type a_constructable, Arg1&& a1, Arg2&& a2)
: A(std::forward<Arg1>(a1))
, p(std::forward<Arg2>(a2))
{
}
template<typename... Args>
B(Args&&... args)
: B(typename can_construct_A<Args&&...>::type()
, std::forward<Args>(args)...) {}
int p = 0;
};
int main()
{
B b1("foo");
B b2("foobar", 1);
}
After seeing that A doesn't have a matching constructor, why doesn't it go back and continue looking for other constructors of B that might match? Are there technical reasons?
简而言之(并且非常简单),当发生重载解析时,编译器会执行以下操作:
展开所有可能匹配给定参数的模板化重载。将它们添加到列表中(权重表示到达那里所涉及的专业化水平)。
将任何具体的重载添加到列表中,这些重载可以通过合法地将转换运算符应用于参数来实现,权重表示将提供的参数转换为函数参数类型需要多少次转换.
按升序 'work' 或权重对列表进行排序。
选择需要最少工作的一项。如果有一个最好的平局,错误。
编译器在这方面做得很好。这不是递归搜索。
我提前向我们中间的纯粹主义者道歉,他们会觉得这种幼稚的解释令人反感:-)
无需赘述,转发构造函数几乎总是首选。它甚至可能比复制构造函数更受欢迎。
避免这种歧义的一种技术是让调用者明确select他们是否需要转发构造函数,通过使用一个虚拟参数:
struct B : A
{
enum dummy_t { forwarding };
// ...
template<typename... Args>
B(dummy_t, Args&&... args) : A(std::forward<Args>(args)...) {}
};
使用示例:
B b2("foobar", 1);
B b(B::forwarding, "foobar");
那么你甚至可以有一个 A
和一个 B
具有相同参数的构造函数。
您的问题的另一种解决方案是在 B
的定义中写入 using A::A;
。这就像为 B
提供一组匹配 A
的构造函数,它们通过使用相同的参数调用相应的 A
构造函数来初始化 A
。
当然这有一些缺点,例如您不能同时初始化其他 B
成员。 Further reading
选项 #1
从 class A
:
struct B : A
{
using A::A;
// ~~~~~~~~~^
B(const std::string& a, int b) : A(a), p(b) {}
int p = 0;
};
选项 #2
使 B 的可变参数构造函数 SFINAE 可用:
#include <utility>
struct B : A
{
template <typename... Args, typename = decltype(A(std::declval<Args>()...))>
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
B(Args&&... args) : A(std::forward<Args>(args)...) {}
B(const std::string& a, int b) : A(a), p(b) {}
B(B& b) : B(static_cast<const B&>(b)) {}
B(const B& b) : A(b) {}
int p = 0;
};