c++ 从函数正确传播`decltype(auto)`变量

pbossiut  于 2023-04-01  发布在  其他
关注(0)|答案(4)|浏览(132)

(This是 Are there any realistic use cases for decltype(auto) variables? 的后续内容)
考虑以下场景-我想将一个函数f传递给另一个函数invoke_log_return,它将:
1.调用f;
1.输出到 stdout;
1.返回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)将是一个 * 纯右值 *(符合副本省略)。
  • f返回 lvaluexvalue 时:
  • 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助手的替代方法(如巴里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/复制省略。
live example on godbolt.org
正如Ben Voigt所提到的,这种方法的一个主要缺点是f的返回值不能成为日志消息的一部分。

g6baxovj

g6baxovj1#

这是最简单明了的写法:

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

svmlkihl

svmlkihl2#

我们可以使用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<F>(f)()返回

  • 一个纯右值,则result是非引用类型,并且invoke_log_return返回非引用类型;
  • 左值,则result是左值引用,并且invoke_log_return返回左值引用类型;
  • xvalue,则result是右值引用,而invoke_log_return返回右值引用类型。

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

e5nqia27

e5nqia273#

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

好吧,证明最优性总是很难的,但是你的第一个解决方案已经很短了。实际上,你唯一希望删除的是if constexpr-其他所有东西都是必要的(w/o改变问题的重点)。
第二个解决方案解决了这个问题,但代价是一些额外的心理扭曲和无法使用日志语句中的变量--或者更一般地说,它只允许您执行与结果无关的操作。
@david-kennedy的简单解决方案通过创建一个纯右值,然后将其复制删除到其最终存储位置,以简洁的方式解决了这个问题。如果您的用例支持此模型并且使用GCC,那么它几乎是最好的解决方案:

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

然而,这种解决方案根本没有实现完美的转发,因为它的返回值与 Package 函数的类型不同(它剥离了引用)。除了是潜在错误的来源(int& a = f();int& a = wrapper(f);)之外,这还导致至少执行一次复制。
为了说明这一点,我修改了测试工具,使其本身不执行任何拷贝。因此,这个GCC输出显示了 Package 器本身所做的拷贝(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
然而,通过摆脱if constexpr并将不同的实现移动到通过enable_if区分的两个函数中,可以创建在GCC和clang上执行零复制/移动操作的解决方案:

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为什么不对上面的纯右值执行NRVO?是否应该将其报告为潜在的增强,或者g的NRVO优化在这里不法律的?”

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

  • 在具有类返回类型的函数[check]中的return语句[check]中[函数模板示例化为返回s的函数,所以check],当 expression 是非易失性[check]自动[check]对象的名称时(除了函数参数或由 * handler*(14.4)[check]的 exception-decleration 引入的变量)(忽略cv-qualification)作为函数返回类型[check],可以通过将自动对象直接构造到函数调用的返回对象中来省略复制/移动操作。

我不知道有任何其他理由可以证明这是无效的。

bvhaajcl

bvhaajcl4#

自从P2266R3在C++23中被接受后,它就变得简单了:

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

它将相应地返回左值、xvalue或右值。
至于为什么clang会出现问题,我观察到autodecltype(auto)函数之前没有执行NRVO。它似乎也不喜欢constexpr if。这是一个clang的实现质量问题。下面显示了clang(C++23)中所需的省略:

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

https://gcc.godbolt.org/z/sKv3vcGbh
请看另一个很棒的答案https://stackoverflow.com/a/63320152/5754656 for C++17/20。他们的invoke_return“修复”了clang中的NRVO,因为它不使用decltype(auto)

相关问题