Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correctly propagating a `decltype(auto)` variable from a function

(This is a follow-up from "Are there any realistic use cases for `decltype(auto)` variables?")

Consider the following scenario - I want to pass a function f to another function invoke_log_return which will:

  1. Invoke f;

  2. Print something to stdout;

  3. Return the result of f, avoiding unnecessary copies/moves and allowing copy elision.

Note that, if f throws, nothing should be printed to stdout. This is what I have so far:

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;     } } 

Let's consider the various possibilities:

  • When f returns a prvalue:

    • result will be an object;

    • invoke_log_return(f) will be a prvalue (eligible for copy elision).

  • When f returns an lvalue or xvalue:

    • result will be a reference;

    • invoke_log_return(f) will be a lvalue or xvalue.

You can see a test application here on godbolt.org. As you can see, g++ performs NRVO for the prvalue case, while clang++ doesn't.

Questions:

  • Is this the shortest possible way of "perfectly" returning a decltype(auto) variable out of a function? Is there a simpler way to achieve what I want?

  • Can the if constexpr { ... } else { ... } pattern be extracted to a separate function? The only way to extract it seems to be a macro.

  • Is there any good reason why clang++ does not perform NRVO for the prvalue case above? Should it be reported as a potential enhancement, or is g++'s NRVO optimization not legal here?


Here's an alternative using a on_scope_success helper (as suggested by 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)(); } 

While invoke_log_return_scope is much shorter, this requires a different mental model of the function behavior and the implementation of a new abstraction. Surprisingly, both g++ and clang++ perform RVO/copy-elision with this solution.

live example on godbolt.org

One major drawback of this approach, as mentioned by Ben Voigt, is that the return value of f cannot be part of the log message.

like image 223
Vittorio Romeo Avatar asked Aug 10 '19 18:08

Vittorio Romeo


People also ask

What is the difference between auto and decltype in C++?

2) decltype Keyword: It inspects the declared type of an entity or the type of an expression. ‘auto’ lets you declare a variable with a particular type whereas decltype lets you extract the type from the variable so decltype is sort of an operator that evaluates the type of passed expression.

When to initialize a variable declared with the auto keyword?

Note: The variable declared with auto keyword should be initialized at the time of its declaration only or else there will be a compile-time error. Note: We have used typeid for getting the type of the variables. Typeid is an operator which is used where the dynamic type of an object needs to be known.

What is decldecltype in C++?

Decltype is auto's not-evil twin. Auto lets you declare a variable with a particular type; decltype lets you extract the type from a variable (or any other expression). What do I mean?

Can I use decltype to return a value from a method?

You can use decltype for pretty much any expression, including to express the type for a return value from a method. Hmm, that sounds like a familiar problem doesn't it? What if we could write:


1 Answers

That's the simplest and most clear way to write it:

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

The GCC gets the right (no needless copies or moves) expected result:

    s()  in main  prvalue     s()     ...logging here... Foo!  lvalue     s(const s&)     ...logging here... Foo!  xvalue     s(s&&)     ...logging here... Foo! 

So if code is clear, have ever the same functionality but is't optimized to run as much as the competitors does it's a compiler optimization failure and clang should work it out. That's the kind of problem that make lot more sense solved in the tool instead the application layer implementation.

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

like image 101
David Kennedy Avatar answered Oct 02 '22 05:10

David Kennedy