0

In this answer they suggest to use the following code:

#include <iostream>

template <typename F>
class Finally {
    F f;
public:
    template <typename Func>
    Finally(Func&& func) : f(std::forward<Func>(func)) {}
    ~Finally() { f(); }

    Finally(const Finally&) = delete;
    Finally(Finally&&) = delete;
    Finally& operator =(const Finally&) = delete;
    Finally& operator =(Finally&&) = delete;
};

template <typename F>
Finally<F> make_finally(F&& f)
{
    return Finally<F>{ std::forward<F>(f) }; // This doesn't compile
    //This compiles: return { std::forward<F>(f) };
}


int main()
{
    auto&& doFinally = make_finally([&] { std::cout<<", world!\n"; });
    std::cout << "Hello";
}

The author linked to a demo that compiles with Clang++/G++ . However, this code does not compile for me in MSVC++2017 .

The error message is:

source_file.cpp(20): error C2280: 'Finally<main::<lambda_9000fb389e10855198e7a01ce16ffa3d>>::Finally(Finally<main::<lambda_9000fb389e10855198e7a01ce16ffa3d>> &&)': attempting to reference a deleted function
source_file.cpp(12): note: see declaration of 'Finally<main::<lambda_9000fb389e10855198e7a01ce16ffa3d>>::Finally'
source_file.cpp(26): note: see reference to function template instantiation 'Finally<main::<lambda_9000fb389e10855198e7a01ce16ffa3d>> make_finally<main::<lambda_9000fb389e10855198e7a01ce16ffa3d>>(F &&)' being compiled
        with
        [
            F=main::<lambda_9000fb389e10855198e7a01ce16ffa3d>
        ]

So what is the difference between return { std::forward<F>(f) }; and return Finally<F>{ std::forward<F>(f) }; that one compiles, but the other doesn't?

Demo

Serge Rogatch
  • 13,865
  • 7
  • 86
  • 158
  • `auto&&` should be supported by MSVC. – Zereges Jun 14 '17 at 17:36
  • 1
    `Finally make_finally(F&& f)` should that not be `Finally> make_finally(F&& f)`? – Justin Jun 14 '17 at 17:44
  • 3
    Why is this compiling with Clang? The return from `make_finally` wants to call the (deleted) move constructor for `Finally`. Event if it is optimized out, it should still need to be available. – 1201ProgramAlarm Jun 14 '17 at 17:47
  • 1
    @1201ProgramAlarm If `make_finally` is a candidate for RVO, since C++17 guarantees RVO, you don't need the move constructor IIRC – Justin Jun 14 '17 at 17:51
  • Am I missing something or this should work with `auto const & doFinally = ...` as well? – yzt Jun 14 '17 at 17:51
  • 3
    People! There is NO moving here. `make_finally()` creates a `Finally` directly in the return object, which is then bound to the reference `doFinally`. No moving happens anywhere, even without the new value categories, even with `-fno-elide-constructors`. – Barry Jun 14 '17 at 18:12
  • The code you've posted [compiles fine on MSVC 2017](https://godbolt.org/g/pnva2c)... – ildjarn Jun 14 '17 at 18:13
  • @Barry That's only true in C++17. Why does this work in C++11 as well? – cdhowie Jun 14 '17 at 18:13
  • @cdhowie That was true in C++11. – Barry Jun 14 '17 at 18:14
  • @cdhowie : That's true of all instances of list-initialization, from C++11 onwards. – ildjarn Jun 14 '17 at 18:14
  • Derp, I forgot that we're binding the return value to a reference. Ignore me. – cdhowie Jun 14 '17 at 18:15
  • I've found that whether it compiles or not depends on whether `return { std::forward(f) };` or `return Finally{ std::forward(f) };` is used. I've updated the question. – Serge Rogatch Jun 14 '17 at 18:47
  • @SergeRogatch : As was said, list-initialization constructs directly into the return value. – ildjarn Jun 14 '17 at 19:06

1 Answers1

4

So what is the difference between return { std::forward<F>(f) }; and return Finally<F>{ std::forward<F>(f) };

The former initializes the return object in place. So when you have:

X foo() { return {a, b, c}; }
auto&& res = foo();

This will construct an X in place and then bind res to it. There is no moving anywhere at all. Not a move that would be elided thanks to RVO, not a move that would be elided thanks to guaranteed elision in C++17 with the new value categories. At no point are we even considering moving anything. There is only ever one X (even in C++11, even with -fno-elide-constructors, because there is no constructor call to elide).

By contrast, this:

X bar() { return X{a, b, c}; }
auto&& res2 = bar();

Before C++17, would create a temporary X and then move it into the return of bar(). This would be a candidate for RVO and the move would surely be elided away, but in order to elide a move, a move has to actually be possible to begin with. And in our case, X's move constructor is deleted so this code is ill-formed. This leads to a situation that is best described by Richard Smith's comment in P0135R0:

// error, can't perform the move you didn't want,
// even though compiler would not actually call it

After C++17, there is no temporary. We have a prvalue of type X that we are using to initialize the return object of bar(), so we end up simply initializing the return object from the prvalue's initializer. The behavior is equivalent to the foo() example above. No move will be done.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • I wouldn't have said "directly initializes", too easy to confuse. – T.C. Jun 14 '17 at 19:14
  • @T.C. Removed the word direct everywhere. Hard to carefully pick words that don't mean different things in different contexts. – Barry Jun 14 '17 at 19:38
  • "There is no moving anywhere at all. Not a move that would be elided thanks to RVO, not a move that would be elided thanks to guaranteed elision in C++17 with the new value categories. ". This is extreme confusing, because you make a difference between "no moving at all" and "moving that is guaranteed to be elided in C++17". Therefore, minus 1 – Johannes Schaub - litb Jun 15 '17 at 13:50
  • @JohannesSchaub-litb Can't please everyone I guess. I'm trying to establish that the reason for the lack of move *isn't* due to C++17. It doesn't help that the actual paper is called "guaranteed copy elision" – Barry Jun 15 '17 at 14:07