C++构造函数:完美的转发和过载

C++ Constructor: Perfect forwarding and overload

本文关键字:转发 构造函数 完美 C++      更新时间:2023-10-16

我有两个类,A和B,B是从A派生的。A有多个构造函数(下面的示例中有2个)。B有一个额外的成员需要初始化(它有一个默认的初始化器)。

如何使用A的一个构造函数来构造B,而不必手动重写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

选项#1

从类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;
};

"foobar"const char (&) [7]。因此Argsconst std::string& 更匹配

因此,该过载被选中:

template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}

其中Argsconst char (&) [7]

所以它变成了:

B(const char (&&args_0) [7], int&& args_1)

它被转发到A的2-参数构造函数。。。这是不存在的。

想要的行为是,如果你用一个为a工作的构造函数来构造B,那么B的"…Args构造函数"就会被调用,否则就会调用B的另一个构造函数,否则就会失败,"找不到适合B的构造函数"。

像这样的。。。

#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);
}

在看到A没有匹配的构造函数之后,为什么不回去继续寻找B的其他可能匹配的构造函数呢?是否存在技术原因?

简而言之(简单地说),当过载解决发生时,编译器会执行以下操作:

  1. 展开所有可能与给定参数匹配的模板重载。将他们添加到一个列表中(用一个权重表示达到目标所涉及的专业化水平)。

  2. 将任何具体的重载添加到列表中,这些重载可以通过合法地将转换运算符应用于参数来实现,并且权重指示将提供的参数转换为函数参数类型所需的转换次数。

  3. 按"功"或权重升序排列列表。

  4. 选择一个需要最少工作的。如果有一个平局是最好的,错误。

编译器可以尝试一下。这不是递归搜索。

我提前向我们当中的纯粹主义者道歉,他们会觉得这种幼稚的解释令人反感:-)

不需要过多的细节,转发构造函数几乎总是首选的。它甚至可能比复制构造函数更可取。

避免这种歧义的一种技术是通过使用一个伪参数:,让调用者明确地选择他们是否想要转发构造函数

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");

然后,您甚至可以拥有一个具有相同参数的AB构造函数。


解决问题的另一种方法是在B的定义中写入using A::A;。这就像为B提供了一组与A匹配的构造函数,它们通过使用相同的参数调用相应的A构造函数来初始化A

当然,这也有一些缺点,例如,您不能同时初始化其他B成员。进一步读取