【问题标题】:How can I tell whether I'm forwarding to a copy constructor?如何判断我是否正在转发到复制构造函数?
【发布时间】:2020-10-26 12:05:02
【问题描述】:

如果我正在编写一个将参数转发给构造函数的通用函数,有没有办法判断它是否是一个复制构造函数?基本上我想做:

template <typename T, typename... Args>
void CreateTAndDoSomething(Args&&... args) {
  // Special case: if this is copy construction, do something different.
  if constexpr (...) { ... }

  // Otherwise do something else.
  ...
}

我想出的最好方法是检查sizeof...(args) == 1,然后查看std::is_same_v&lt;Args..., const T&amp;&gt; || std::is_same_v&lt;Args..., T&amp;&gt;。但我认为这会遗漏一些边缘情况,例如 volatile 限定的输入和可隐式转换为 T 的东西。

说实话,我并不完全确定这个问题是否明确,所以请随时告诉我它不是(以及为什么)。如果有帮助,您可以假设 T 的唯一单参数构造函数是 T(const T&amp;)T(T&amp;&amp;)

如果我是正确的,因为复制构造函数不是事物,所以这不是很好的定义,那么也许可以通过说“我如何判断表达式 T(std::forward&lt;Args&gt;(args)...) 是否选择重载接受const T&amp;

【问题讨论】:

  • 当只有一个 Args 并且是 T 时,您不能提供单独的重载吗?
  • 如果是副本,为什么现在需要?也许一些几乎复制但不完全的情况也应该属于同一个桶? IOW,是 XY 问题吗?
  • 很抱歉没有排除 XY 问题。基本前提是我正在解决 API 的错误设计:我使用的 API 要求调用者在复制对象时执行不同的操作,而不是使用 API 的所有其他使用,这非常不规则。我想将它包装在我自己的层中,通过检测适当的重载的调用并代表用户做一些特殊的事情来规范这一点。它是一个复制构造函数这一事实并不重要。我只需要知道将选择哪个重载。
  • 这个问题没有那么明确,因为复制构造函数不一定需要一个参数。只要第一个参数是 const 左值引用并且其他参数是默认的,您仍然有一个复制构造函数。请参阅class.copy.ctor。因此,对sizeof...(args) == 1 的检查在一般意义上在技术上并不正确。
  • 也就是说,复制构造调用了一个符合复制构造函数的构造函数。但是调用符合复制构造函数的构造函数不一定是复制构造,如果提供的参数数量!= 1,那么它不是复制构造。转发sizeof...(args) &gt; 1 永远不会“转发到复制构造函数”,即使它转发到也是复制构造函数的构造函数,它在此调用中不充当复制构造函数。

标签: c++ templates


【解决方案1】:

你可以使用 remove_cv_t:

#include <type_traits>

template <typename T, typename... Args>
void CreateTAndDoSomething(Args&&... args) {
  // Special case: if this is copy construction, do something different.
  if constexpr (sizeof...(Args) == 1 && is_same_v<T&, remove_cv_t<Args...> >) { ... }

  // Otherwise do something else.
  ...
}

这涵盖了所有“复制构造函数”作为标准的defined,不考虑可能的默认参数(很难确定给定的函数参数——对于给定这些参数将被调用的函数——是否是默认的与否)。

【讨论】:

  • 默认参数没有问题,它们不会通过CreateTAndDoSomething层转发,不会计入sizeof...(Args),它们是编译器在调用T(std::forward&lt;Args&gt;(args)...)时插入的
  • @BenVoigt 当然,但是如果你有一个构造函数A(const A&amp;, int = 0),它是一个符合标准的复制构造函数,即使你像new A(other_a, 123) 那样调用它。在这种情况下,如果Argsconst A&amp;, int,它应该算作复制构造函数,但在我目前的代码中,它没有。
  • 谢谢!我看到这个解决方案没有涵盖默认参数,但在我的特殊情况下,我不需要担心它们(我可以假设T 没有)。
  • 实际上我想到这个答案不包括隐式转换。是否可以在调用之前判断重载选择是否会导致参数隐式转换为目标类型?
  • 好吧,它不会是一个复制构造函数,但是你可以使用 is_convertible_v 代替 is_same_v。
【解决方案2】:

你的想法是对的。所需的一切都以Args 的推断类型编码。但是,如果您想考虑所有符合 cv 条件的案例,那么还有很多事情要做。让我们首先识别可能出现的不同情况:

  1. 构造(隐式转换是构造)
  2. 复制构造(常用T(const T&amp;)
  3. 移动构造(常用T(T&amp;&amp;)
  4. 切片(使用Derived 调用Base(const Base&amp;)Base(Base&amp;&amp;)

如果不考虑奇怪的移动或复制构造函数(带有默认参数的构造函数),则情况 2-4 可能只会发生一个参数被传递,其他一切都是构造函数。因此,为单参数情况提供重载是明智的。尝试在可变参数模板中处理所有这些情况会很糟糕,因为您必须使用折叠表达式或 std::conjuction/std::disjuction 之类的东西才能使 if 语句有效。

我们还会发现,在每种情况下分别识别移动和复制是不可能的。如果不需要单独考虑复制和移动,解决方案很简单。但是,如果需要将这些情况分开,只能做出一个很好的猜测,这应该几乎总是有效。

关于切片,我可能会选择使用static_assert 禁用它。

移动和复制结合

这是使用单个参数重载的解决方案。接下来我们详细介绍一下。

#include <utility>
#include <type_trait>
#include <iostream>


// Multi-argument case is almost always construction
template<typename T, typename... Args>
void CreateTAndDoSomething(Args&&... args)
{   
    std::cout << "Constructed" << '\n';
    T val(std::forward<Args>(args)...);
}

template<typename T, typename U>
void CreateTAndDoSomething(U&& arg)
{
    // U without references and cv-qualifiers
    // std::remove_cvref_t in C++20
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;

    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<StrippedU, T>)
    {
        std::cout << "Copied or moved" << '\n';
    }
    else
    {
        std::cout << "Constructed" << '\n';
    }
    
    T val(std::forward<U>(arg));
}

这里我们利用U&amp;&amp;(和Args&amp;&amp;)是一个转发引用这一事实。使用转发引用,推导出的模板参数U 会根据传递的arg 的值类别而有所不同。给定一个T 类型的arg,推导出U

  • 如果arg 是左值,则推导出的UT&amp;(包括cv-限定符)。
  • 如果arg 是一个右值,则推导出的UTcv-包括限定符)。

注意: U 可能会推断为 cv 限定的引用(例如 const Foo&amp;)。 std::remove_cv 只删除顶级 cv-限定符,引用不能有顶级 cv-限定符。这就是为什么std::remove_cv 需要应用于非引用类型的原因。如果只使用std::remove_cv,模板将无法识别Uconst T&amp;volatile T&amp;const volatile T&amp; 的情况。

只复制

U 被推导为T&amp; const T&amp;volatile T&amp;const volatile T&amp; 时,将调用复制构造函数(通常,见注释)。因为我们有三种情况,其中推导出的 U 是一个 cv 限定引用,而 std::remove_cv 不适用于这些情况,我们应该明确检查这些情况:

template<typename T, typename U>
void CreateTAndDoSomething(U&& arg)
{
    // U without references and cv-qualifiers
    // std::remove_cvref_t in C++20
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;

    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<T&, U> 
        || std::is_same_v<const T&, U>
        || std::is_same_v<volatile T&, U>
        || std::is_same_v<const volatile T&, U>)
    {
        std::cout << "Copied" << '\n';
    }
    else
    {
        std::cout << "Constructed" << '\n';
    }
    
    T val(std::forward<U>(arg));
}

注意: 当移动构造函数不可用且具有签名T(const T&amp;) 的复制构造函数可用时,这不会识别复制构造。这是因为使用右值arg 调用std::forward 的结果是一个xvalue,可以绑定到const T&amp;

移动和复制分开

免责声明:此解决方案仅适用于一般情况(请参阅陷阱)

假设T 有一个带有签名T(const T&amp;) 的复制构造函数和带有签名T(T&amp;&amp;) 的移动构造函数,这很常见。 const-qualified 移动构造函数实际上没有意义,因为移动的对象需要几乎总是进行修改。

在此假设下,表达式T val(std::forward&lt;U&gt;(arg)); 移动构造val,如果U 被推导出为非常量Targ 是非常量右值)。这给了我们两种情况:

  1. U 推导出为T
  2. U 推导出为volatile T

通过首先从U 中删除 volatile 限定符,我们可以解决这两种情况。当移动构造首先识别时,其余的都是复制构造:

template<typename T, typename U>
void CreateTAndDoSomething(U&& arg)
{
    // U without references and cv-qualifiers
    using StrippedU = std::remove_cv_t<std::remove_reference_t<U>>;
    
    // Extra check is needed because T is a base for itself
    static_assert(
        std::is_same_v<StrippedU, T> || !std::is_base_of_v<T, StrippedU>, 
        "Attempting to slice"
    );
    
    if constexpr (std::is_same_v<std::remove_volatile_t<U>, T>)
    {
        std::cout << "Moved (usually)" << '\n';
    }
    else if constexpr (std::is_same_v<StrippedU, T>)
    {
        std::cout << "Copied (usually)" << '\n';
    }
    else
    {
        std::cout << "Constructed" << '\n';
    }
    
    T val(std::forward<U>(arg));
}

如果您想试用该解决方案,请访问godbolt。我还实现了一个特殊的类,希望有助于可视化不同的构造函数调用。

解决方案的缺陷

当前面所述的假设不成立时,无法准确确定调用的是复制构造函数还是移动构造函数。至少有一些特殊情况会导致歧义:

  1. 如果T 的移动构造函数不可用,则argT 类型的右值,并且复制构造函数具有签名T(const T&amp;)

    std::forward&lt;U&gt;(arg) 返回的 xvalue 将绑定到 const T&amp;。这也在“唯一副本”案例中进行了讨论。

    已识别移动,但发生复制。

  2. 如果T 有一个带有签名T(const T&amp;&amp;) 的移动构造函数并且argT 类型的const 右值:

    复制已识别,但发生了移动。与T(const volatile T&amp;&amp;).类似的情况

当用户明确指定 UT&amp;&amp;volatile T&amp;&amp; 将编译但无法正确识别)时,我还决定不考虑这种情况。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-17
    • 1970-01-01
    相关资源
    最近更新 更多