参数包模板化构造函数删除复制分配

parameter pack templated constructor deletes copy assignment

提问人:user1470475 提问时间:5/9/2023 最后编辑:fabianuser1470475 更新时间:5/9/2023 访问量:61

问:

试图理解为什么为类使用参数包模板化构造函数显然会导致复制构造函数和复制赋值运算符都被优化。(实际上,我可以看到编译器如何无法将复制构造函数签名与模板化构造函数区分开来,但是当使用复制赋值运算符时,这似乎应该很明显)

示例代码”

#include <array>
#include <iostream>


struct A
{
    std::array<int,3> a;

    template<typename ... Args>
    A(Args&& ... args)
        :   a{args ...}
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    
    A(const A& other)
        :   a{other.a}
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A& operator=(const A& other)
    {
        a = other.a;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        return *this;
    }
};

int main()
{
    A a(1,2.f,3.);
    A b = a; // fails (see compiler output)
    //A c(a); // also fails (templated constructor is better match)
}

编译输出:

templated_constructror.cpp: In instantiation of ‘A::A(Args&& ...) [with Args = {A&}]’:
templated_constructror.cpp:35:8:   required from here
templated_constructror.cpp:11:15: error: cannot convert ‘A’ to ‘int’ in initialization
   11 |   : a{args ...}
C++ Templates 构造函数 copy-assignment

评论

0赞 Eljay 5/9/2023
为什么而不是?A(Args&&... args)A(Args... args)
1赞 Oersted 5/9/2023
A(Args&& ... args)是比 更好的匹配项。如果我理解得很好,那是因为你在输入中有一个 A,可以由第一个构造函数推导,而复制构造函数意味着从非常量到常量的“转换”。为了解决这个问题,你可以看看Scott Meyers的《有效的现代C++》中的第27项。A(const A& other)
1赞 fabian 5/9/2023
@Eljay不确定 OP 的意图,但如果一个类型有一个复制构造函数和一个非显式转换运算符,可以在 const 限定的对象上使用,这可能会有所不同:如果你使用 ,将制作一个可能昂贵的副本,但参数将通过引用传递......intArgs...Args&&...

答:

2赞 fabian 5/9/2023 #1

由于您正在同一个表达式中执行声明和赋值,

A b = a

使用构造函数。

由于是非常量限定的,因此,在给定传递给构造函数的参数的情况下,对非常量 () 进行左值引用的构造函数是更好的匹配。aA&

如果您实际上将左值引用传递给 const ,则代码将编译:A

A b = static_cast<A const&>(a);

如果希望它按预期工作,请添加一个构造函数重载,其参数类型为 :A&


struct A
{
    std::array<int, 3> a;

    template<typename ... Args>
    A(Args&& ... args)
        : a{ static_cast<int>(args) ... }
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A(const A& other)
        : a{ other.a }
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A(A& other)
        : A(static_cast<A const&>(other))
    {}

    A& operator=(const A& other)
    {
        a = other.a;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        return *this;
    }
};

int main()
{
    A a(1, 2.f, 3.);
    A b = a;
    A c(a);
}

或者,如果提供了单个构造函数参数,则可以按如下方式更改构造函数集,并且构造函数模板是匹配项。(第二个和第三个签名可以使用带有默认参数的构造函数一次性实现。

A(A const&);
A();
A(int);

template<class Arg0, class Arg1, class...Args>
A(Arg0&&, Arg1&&, Args&&...);
struct A
{
    std::array<int, 3> a {};

    A(A const&) = default;

    A(int a0 = 0)
        : a{a0}
    {
    }

    template<class Arg0, class Arg1, class...Args>
    A(Arg0&& arg0, Arg1&& arg1, Args&&... args)
        : a{ static_cast<int>(arg0), static_cast<int>(arg1), static_cast<int>(args)...}
    {
    }
};
2赞 Hari 5/9/2023 #2

请参阅Scott Meyers的“有效的现代C++”中的第26项:避免通用引用过载。当编译器执行重载解析时,基于通用引用(也称为转发引用)的构造函数将是编译器首选的函数。此外,基于通用引用的构造函数本质上是可变的,与此讨论无关。

因此,将导致编译器调用一个构造函数,该构造函数将获取 A 的副本,并且不会调用复制赋值运算符。这是因为在此语句中创建了变量 b。创建一个临时的,然后分配给它的工作量太大了。A b = a;

鉴于模板化构造函数的存在,编译器必须经历重载解析过程以找到最佳函数。它将实例化模板化构造函数以采用非常量左值。然后,在构造函数(实例化的模板化构造函数)和(已经存在的复制构造函数)之间,它将选择实例化的构造函数,因为它更简单(无需向 A& 添加常量)。A&A&const A&

可以约束采用通用引用的构造函数,以便仅当传递要分配给基础数组的值列表时才会调用它。它可以通过enable_if(再次参考上述斯科特·迈耶的书的第 27 项)或通过 C++20 中的概念来实现。Dharmesh946 的答案展示了一种基于enable_if的方法。基于概念(C++20)的方法是

#include <concepts>

...

    template<typename ... Args>
    requires (std::is_convertible_v<Args, int> && ...) 
    A(Args&& ... args)
        :   a{args ...}
    {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

在上面的代码中,使用了折叠表达式来检查包中的每个参数是否可以转换为整数类型。如果包有一个或多个参数,这些参数都可以转换为 int,则实例化并调用此构造函数。

2赞 Oersted 5/9/2023 #3

可能的解决方法:

#include <array>
#include <iostream>
#include <type_traits>

struct A {
    std::array<int, 3> a;

    template <typename... Args,
              std::enable_if_t<sizeof...(Args) != 1, bool> = true>
    A(Args&&... args) : a{args...} {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
    template <typename Arg,
              std::enable_if_t<!std::is_same<std::decay_t<Arg>, A>::value,
                               bool> = true>
    A(Arg&& arg) : a{arg} {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A(const A& other) : a{other.a} {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A& operator=(const A& other) {
        a = other.a;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        return *this;
    }
};

int main() {
    A a(1, 2.f, 3.);
    A b = a;  // now OK
    A c(a); // now OK
}

它当然可以改进,我不确定这是你想要实现的。 基本上,第一个构造函数只捕获带有 0 或 2+ 参数的初始化。 如果第二个参数不是 A,则仅与一个参数一起使用。 第三个是普通的复制构造函数。现场演示