具有值包装器的可变参数模板构造函数的类构造函数优先级

class constructor precedence with a variadic template constructor for a value wrapper

本文关键字:构造函数 优先级 参数 包装 变参      更新时间:2023-10-16

>今天我发现我不明白构造函数优先级规则C++。

请参阅以下模板struct wrapper

template <typename T>
struct wrapper
{
T value;
wrapper (T const & v0) : value{v0}
{ std::cout << "value copy constructor" << std::endl; }
wrapper (T && v0) : value{std::move(v0)}
{ std::cout << "value move constructor" << std::endl; }
template <typename ... As>
wrapper (As && ... as) : value(std::forward<As>(as)...)
{ std::cout << "emplace constructor" << std::endl; }
wrapper (wrapper const & w0) : value{w0.value}
{ std::cout << "copy constructor" << std::endl; }
wrapper (wrapper && w0) : value{std::move(w0.value)}
{ std::cout << "move constructor" << std::endl; }
};

它是一个简单的模板值包装器,包括复制构造函数(wrapper const &),移动构造函数(wrapper && w0),一种值复制构造函数(T const & v0),一种移动构造函数(T && v0)和一种模板构造就地构造值构造函数(As && ... as,遵循STL容器的emplace方法的示例)。

我的目的是使用带有包装器的复制或移动构造函数调用,值复制或移动构造函数传递T对象,模板使用能够构造T类型的对象的值列表进行构造函数调用。

但我没有得到我所期望的。

来自以下代码

std::string s0 {"a"};
wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w1{std::move(s0)}; // value move constructor
wrapper<std::string> w2{1u, 'b'};       // emplace constructor
//wrapper<std::string> w3{w0};          // compilation error (?)
wrapper<std::string> w4{std::move(w0)}; // move constructor

w1w2w4值分别按预期使用值移动构造函数、放置构造函数和移动构造函数构造

。但是w0是用 emplace 构造函数构造的(我期待值复制构造函数),w3根本没有构造(编译错误),因为 emplace 构造函数是首选,但不是接受wrapper<std::string>值的std::string构造函数。

第一个问题:我做错了什么?

我想w0问题是因为s0不是const值,所以T const &不完全匹配。

确实,如果我写

std::string const s1 {"a"};
wrapper<std::string> w0{s1};  

我得到名为的值复制构造函数

第二个问题:我必须做什么才能获得我想要的东西?

那么我必须做什么才能使值复制构造函数(T const &)优先于emplace构造函数(As && ...)也具有不恒定的T值,并且大多数情况下,我必须做什么才能使复制构造函数(wrapper const &)优先构造w3

没有"构造函数优先级规则"这样的东西,构造函数在优先级方面没有什么特别之处。

这两个问题案例具有相同的基本规则来解释它们:

wrapper<std::string> w0{s0};            // emplace constructor (?)
wrapper<std::string> w3{w0};            // compilation error (?)

对于w0,我们有两个候选者:值复制构造函数(它接受一个std::string const&)和 emplace 构造函数(它需要一个std::string&)。后者是更好的匹配,因为它的引用比值复制构造函数的引用(特别是 [over.ics.rank]/3)更不符合 cv 条件。一个简短的版本是:

template <typename T> void foo(T&&); // #1
void foo(int const&);                // #2
int i;
foo(i); // calls #1

类似地,对于w3,我们有两个候选者:emplace构造函数(接受wrapper&)和复制构造函数(接受wrapper const&)。同样,由于相同的规则,首选 emplace 构造函数。这会导致编译错误,因为value实际上无法从wrapper<std::string>构造。

这就是为什么在转发引用时必须小心并约束函数模板的原因!这是有效的现代C++中的第 26 项("避免通用引用过载")和第 27 项("熟悉通用引用过载的替代方案")。最低限度是:

template <typename... As,
std::enable_if_t<std::is_constructible<T, As...>::value, int> = 0>
wrapper(As&&...);

这允许w3,因为现在只有一个候选人。w0替换而不是副本这一事实无关紧要,最终结果是一样的。实际上,值复制构造函数无论如何都不会真正完成任何事情 - 您应该将其删除。


我会推荐这组构造函数:

wrapper() = default;
wrapper(wrapper const&) = default;
wrapper(wrapper&&) = default;
// if you really want emplace, this way
template <typename A=T, typename... Args,
std::enable_if_t<
std::is_constructible<T, A, As...>::value &&
!std::is_same<std::decay_t<A>, wrapper>::value
, int> = 0>
wrapper(A&& a0, Args&&... args)
: value(std::forward<A>(a0), std::forward<Args>(args)...)
{ }
// otherwise, just take the sink
wrapper(T v)
: value(std::move(v))
{ }

这样可以以最小的大惊小怪和混乱完成工作。请注意,放置和接收器构造函数是互斥的,请只使用其中之一。

正如 OP 建议的那样,将我的评论作为答案并进行一些详细说明。

由于执行重载解析和类型匹配的方式,通常会选择可变参数前向引用类型的构造函数作为最佳匹配。发生这种情况是因为所有常量限定都将正确解析并形成完美匹配 - 例如,将常量引用绑定到非常量左值等 - 就像您的示例中一样。

处理它们的一种方法是在参数列表与任何其他可用构造函数匹配(尽管不完美)时禁用(通过我们可以使用的各种 sfinae 方法)可变参数构造函数,但这非常乏味,并且每当添加额外的构造函数时都需要持续的支持。

我个人更喜欢基于标签的方法,并使用标签类型作为可变参数构造函数的第一个参数。虽然任何标签结构都可以工作,但我倾向于(懒惰地)从 C++17 -std::in_place中窃取一个类型。代码现在变为:

template<class... ARGS>
Constructor(std::in_place_t, ARGS&&... args)

比被称为

Constructor ctr(std::in_place, /* arguments */);

由于根据我在调用位置的经验,构造函数的性质始终是已知的 - 即您将始终知道您是否打算调用前向引用接受构造函数 - 这个解决方案对我来说效果很好。

正如评论中所说,问题是可变参数模板构造函数 通过转发引用获取参数,因此它与非常量左值副本或常量右值副本更匹配。

有很多方法可以禁用它,一种有效的方法是始终按照 SergeyA 在其答案中提出的建议in_place_t使用标签。另一种方法是在模板构造函数与复制构造函数的签名匹配时禁用模板构造函数,正如著名的有效C++书籍中提出的那样。

在这种情况下,我更喜欢声明复制/移动构造函数(以及复制/移动赋值)的所有可能的签名。这样,无论我向类中添加什么新构造函数,我都不必考虑避免复制构造,它只有短 2 行代码,易于阅读,并且不会污染其他构造函数的接口:

template <typename T>
struct wrapper
{
//...
wrapper (wrapper& w0) : wrapper(as_const(w0)){}
wrapper (const wrapper && w0) : wrapper(w0){}
};

注意:如果计划将其用作易失性类型,或者满足以下所有条件,则不应使用此解决方案:

  • 对象大小小于 16 字节(或 MSVC ABI 的 8 字节),
  • 所有成员子弹都是微不足道的可复制的,
  • 此包装器将被传递给函数,其中特别注意参数是可复制类型且其大小低于前一个阈值的情况,因为在这种情况下,可以通过按值传递参数在寄存器(或两个)中传递参数!

如果满足所有这些要求,那么您可以考虑实现不太可维护的(容易出错 ->下次修改代码时)或客户端接口污染解决方案!