【问题标题】:Correctly propagating a `decltype(auto)` variable from a function从函数中正确传播 `decltype(auto)` 变量
【发布时间】:2019-12-18 01:42:42
【问题描述】:

(这是Are there any realistic use cases for `decltype(auto)` variables?的后续报道)

考虑以下场景 - 我想将函数 f 传递给另一个函数 invoke_log_return,它将:

  1. 调用f

  2. 打印一些东西到stdout;

  3. 返回f的结果,避免不必要的复制/移动并允许复制省略。

请注意,如果f 抛出,则不应将任何内容打印到stdout。这是我目前所拥有的:

template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");

    if constexpr(std::is_reference_v<decltype(result)>)
    {
        return decltype(result)(result);
    }
    else
    {
        return result;
    }
}

让我们考虑各种可能性:

  • f 返回一个prvalue

    • result 将是一个对象;

    • invoke_log_return(f) 将是 prvalue(符合复制省略条件)。

  • f 返回一个左值x值时:

    • result 将作为参考;

    • invoke_log_return(f) 将是 lvaluexvalue

您可以看到一个测试应用程序here on godbolt.org。如您所见,g++prvalue 情况执行 NRVO,而 clang++ 不执行。

问题:

  • 这是从函数中“完美”返回 decltype(auto) 变量的最短方法吗?有没有更简单的方法来实现我想要的?

  • 可以将if constexpr { ... } else { ... } 模式提取到单独的函数中吗?提取它的唯一方法似乎是宏。

  • clang++ 对上述 prvalue 案例不执行 NRVO 有什么好的理由吗? 是否应该将其报告为潜在的增强功能,或者是g++ 的 NRVO 优化在这里不合法?


这是使用 on_scope_success 助手的替代方法(如 Barry Revzin 所建议):

template <typename F>
struct on_scope_success : F
{
    int _uncaught{std::uncaught_exceptions()};

    on_scope_success(F&& f) : F{std::forward<F>(f)} { }

    ~on_scope_success()
    {
        if(_uncaught == std::uncaught_exceptions()) {
            (*this)();
        }
    }
};

template <typename F>
decltype(auto) invoke_log_return_scope(F&& f)
{
    on_scope_success _{[]{ std::printf("    ...logging here...\n"); }};
    return std::forward<F>(f)();
}

虽然invoke_log_return_scope 要短得多,但这需要函数行为的不同心智模型和新抽象的实现。令人惊讶的是,g++clang++ 都使用此解决方案执行 RVO/copy-elision。

live example on godbolt.org

正如Ben Voigt 所提到的,这种方法的一个主要缺点是f 的返回值不能是日志消息的一部分。

【问题讨论】:

  • on_scope_success 也处理 void 返回类型。
  • decltype(result)(result) 是不是有点多余?不返回(结果)做同样的事情。返回类型为 decltype(auto) 的函数和带有括号内值的 return 语句会自动返回一个引用。
  • 作用域成功变体的缺点是无法记录返回值!
  • 对我来说更有趣的问题是:gcc 的 NRVO 是否允许?为什么clang不做呢?
  • @engf-010: return (result); 总是返回一个 lvalue,在 f 产生 xvalue.

标签: c++ c++17 auto copy-elision decltype-auto


【解决方案1】:

这是最简单明了的写法:

template <typename F>
auto invoke_log_return(F&& f)
{ 
    auto result = f();
    std::printf("    ...logging here... %s\n", result.foo());    
    return result;
}

GCC 得到了正确的(没有不必要的复制或移动) 预期结果:

    s()

in main

prvalue
    s()
    ...logging here... Foo!

lvalue
    s(const s&)
    ...logging here... Foo!

xvalue
    s(s&&)
    ...logging here... Foo!

因此,如果代码清晰,具有相同的功能,但没有像竞争对手那样优化运行,这是编译器优化失败,clang 应该可以解决。这种问题在工具而不是应用层实现中解决起来更有意义。

https://gcc.godbolt.org/z/50u-hT

【讨论】:

  • 但关键是原始版本不会对静态实例进行不必要的复制。因此它也支持既不能复制也不能移动的类型。
  • 看看GCC编译输出执行有没有不必要的拷贝。而且原版的日志部分的执行顺序有些问题。
【解决方案2】:

我们可以使用std::forward的修改版:(避免名称转发以防止ADL问题)

template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
    return std::forward<T>(arg);
}

此函数模板用于转发decltype(auto) 变量。可以这样使用:

template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");
    return my_forward<decltype(result)>(result);
}

这样,如果std::forward&lt;F&gt;(f)()返回

  • 一个prvalue,那么result是一个非引用,invoke_log_return返回一个非引用类型;

  • 一个左值,那么result是一个左值引用,invoke_log_return返回一个左值引用类型;

  • 一个xvalue,那么result是一个右值引用,invoke_log_return返回一个右值引用类型。

(基本上是从我的https://stackoverflow.com/a/57440814复制过来的)

【讨论】:

  • 这不允许复制省略。 return 语句的操作数不是命名函数局部变量的 id 表达式。所以这是一种悲观。这也是做更简单的无条件 return decltype(result)(result); 的一种相当冗长的方式
  • @StoryTeller return decltype(result)(result); 也不允许复制省略。所以我认为没有简单的方法可以在这里给复制省略一个镜头,除了让它成为一个单独的分支......
【解决方案3】:

Q1:“这是从函数中“完美”返回 decltype(auto) 变量的最短方法吗?有没有更简单的方法来实现我想要的?”

嗯,证明最优性总是很困难,但你的第一个解决方案已经很短了。实际上,您唯一希望删除的是if constexpr - 其他一切都是必要的(不改变问题的重点)。

您的第二个解决方案以一些额外的心理扭曲和无法在 log 语句中使用变量为代价解决了这个问题 - 或者,更一般地说,它只使您能够执行与您的结果无关的操作。

@david-kennedy 的简单解决方案通过创建一个纯右值,然后可以复制到其最终存储位置,巧妙地解决了这个问题。如果您的用例支持此模型并且您使用 GCC,那么它几乎是最好的解决方案:

template <typename F>
auto invoke_log_return(F&& f)
{ 
    auto result = f();
    std::printf("    ...logging here...\n");    
    return result;
}

但是,此解决方案根本没有实现完美转发,因为它的返回值与包装函数的类型不同(它会剥离引用)。除了成为潜在错误的来源(int&amp; a = f();int&amp; a = wrapper(f);)之外,这还会导致至少执行一个副本。

为了展示这一点,我修改了测试工具,使其本身不执行任何复制。因此,此 GCC 输出显示包装器本身完成的副本(clang 执行更多的复制/移动操作):

    s()
in main

prvalue
    s()
    ...logging here...

lvalue
    s(const s&)
    ...logging here...

xvalue
    s(s&&)
    ...logging here...

https://gcc.godbolt.org/z/dfrYT8

但是,可以创建一个在 GCC 和 clang 上执行零复制/移动操作的解决方案,方法是摆脱 if constexpr 并将不同的实现移动到通过 enable_if 区分的两个函数中:

template <typename F>
auto invoke_log_return(F&& f)
    -> std::enable_if_t<
        std::is_reference_v<decltype(std::forward<F>(f)())>,
        decltype(std::forward<F>(f)())
    >
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging glvalue...\n");
    return decltype(result)(result);
}

template <typename F>
auto invoke_log_return(F&& f)
    -> std::enable_if_t<
        !std::is_reference_v<decltype(std::forward<F>(f)())>,
        decltype(std::forward<F>(f)())
    >
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging prvalue...\n");
    return result;
}

零份:

    s()
in main

prvalue
    s()
    ...logging prvalue...

lvalue
    ...logging glvalue...

xvalue
    ...logging glvalue...

https://gcc.godbolt.org/z/YKrhbs

当然,与原始解决方案相比,这增加了行数,尽管它返回的变量可以说是“更完美”(从两个编译器都执行 NRVO 的意义上说)。将功能提取到实用函数中会导致您的第二个问题。

Q2:“if constexpr { ... } else { ... } 模式可以提取到单独的函数中吗?提取它的唯一方法似乎是宏。”

不,因为您不能省略将纯右值传递给函数,这意味着将result 传递给函数将导致复制/移动。对于 glvalues 这不是问题(如 std::forward 所示)。

但是,可以稍微改变之前解决方案的控制流程,使其本身可以用作库函数:

template <typename F>
decltype(auto) invoke_log_return(F&& f) {
    return invoke_return(std::forward<F>(f), [](auto&& s) {
        std::printf("    ...logging value at %p...", static_cast<void*>(&s));
    });
}

https://gcc.godbolt.org/z/c5q93c

这个想法是使用enable_if 解决方案来提供一个函数,该函数接受一个生成器函数和一个可以对临时值进行操作的附加函数——无论是prvalue、xvalue还是lvalue。库函数可能如下所示:

template <typename F, typename G>
auto invoke_return(F&& f, G&& g)
    -> std::enable_if_t<
        std::is_reference_v<decltype(std::forward<F>(f)())>,
        decltype(std::forward<F>(f)())
    >
{
    decltype(auto) result{std::forward<F>(f)()};
    std::forward<G>(g)(decltype(result)(result));
    return decltype(result)(result);
}

template <typename F, typename G>
auto invoke_return(F&& f, G&& g)
    -> std::enable_if_t<
        !std::is_reference_v<decltype(std::forward<F>(f)())>,
        decltype(std::forward<F>(f)())
    >
{
    decltype(auto) result{std::forward<F>(f)()};
    std::forward<G>(g)(result);
    return result;
}

Q3:“clang++ 对上述prvalue情况不执行NRVO有什么好的理由吗?应该报告为潜在的增强,还是g++的NRVO优化在这里不合法?”

检查我的 C++2a 草案(N4835 §11.10.5/1.1 [class.copy.elision]),NRVO 的表述非常简单:

  • return 语句中 [check] 在具有类返回类型的函数 [check] 中 [函数模板实例化为返回 s 的函数,因此检查],当 表达式 是非易失性 [check] 自动 [check] 对象的名称(除了 * handler* (14.4) [check] 的 exception-decleration 引入的函数参数或变量) 与函数返回类型 [check] 具有相同类型(忽略 cv 限定),则可以通过将自动对象直接构造到函数调用的返回对象中来省略复制/移动操作。

我不知道这应该是无效的任何其他原因。

【讨论】:

    猜你喜欢
    • 2014-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-17
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多