I've come across an unexpected problem while creating a trivial RAII wrapper.
Let alone the logical incompleteness of the code below (copy-constructor and assignment operator not deleted etc., this is meant to be an SSCCE), what strikes me is that copy-initialization of my wrapper with a temporary lambda results in a compilation error, while direct-initialization does not.
This behavior can be observed both on GCC 4.7.2 and on Clang 3.2, while ICC 13.0.1 and VC10 compile both versions without problems.
#include <iostream>
#include <functional>
using namespace std;
struct A
{
template<typename F>
A(F&& f) : _f(forward<F>(f)) { }
~A() { _f(); }
private:
std::function<void()> _f;
};
int main()
{
// A a = [] () { cout << "Hello" << endl; }; // ERROR!
A a([] () { cout << "Hello" << endl; }); // OK
}
Who is right, and what is the problem with those who are wrong? Is it an issue with the implementation of the C++ Standard Library, or rather a compiler issue?
References to the C++11 Standard are particularly welcome.
EDIT:
Here is the error produced by Clang 3.2:
Compilation finished with errors:
In file included from source.cpp:2:
/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/functional:1925:2: error: type 'A' does not provide a call operator
(*_Base::_M_get_pointer(__functor))(
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../include/c++/4.7/functional:2297:33: note: in instantiation of member function 'std::_Function_handler<void (), A>::_M_invoke' requested here
_M_invoker = &_My_handler::_M_invoke;
^
source.cpp:9:16: note: in instantiation of function template specialization 'std::function<void ()>::function<A>' requested here
A(F&& f) : _f(forward<F>(f)) { }
^
source.cpp:20:7: note: in instantiation of function template specialization 'A::A<A>' requested here
A a = [] () { cout << "Hello" << endl; }; // ERROR!
^
1 error generated.
The error message (gcc 4.7.2) is reasonably informative:
c++/4.7/functional: In instantiation of 'static void std::_Function_handler<void(_ArgTypes ...), _Functor>::_M_invoke(const std::_Any_data&, _ArgTypes ...) [with _Functor = A; _ArgTypes = {}]':
c++/4.7/functional:2298:6: required from 'std::function<_Res(_ArgTypes ...)>::function(_Functor, typename std::enable_if<(! std::is_integral<_Functor>::value), std::function<_Res(_ArgTypes ...)>::_Useless>::type) [with _Functor = A; _Res = void; _ArgTypes = {}; typename std::enable_if<(! std::is_integral<_Functor>::value), std::function<_Res(_ArgTypes ...)>::_Useless>::type = std::function<void()>::_Useless]'
source.cpp:9:32: required from 'A::A(F&&) [with F = A]'
source.cpp:22:44: required from here
c++/4.7/functional:1926:2: error: no match for call to '(A) ()'
The problem is that your class does not have an implicit move constructor available for use in copy-initialization. The implicitly defined move constructor is deleted, since A
has a user-defined destructor (12.8p9b4).
Add:
A(A &&) = default;
Note that since the defaulted move constructor is selected, the destructor should check that _f
is non-empty; since the move constructor of std::function
does not guarantee that the target is left empty, you should also perform that change yourself:
A(A &&a): _f() { std::swap(_f, a._f); }
~A() { if (_f) _f(); }
Recall that (per 8.5p17) a copy-initialization involves the creation of a prvalue temporary, which is then used to direct-initialize the target object. The choice is between the template constructor and the implicitly defined copy constructor; the template constructor with type template argument A
is preferred, since A &&
binds to prvalue A
better than const A &
does.
An alternative (possibly better) is to disable the template constructor for A
arguments:
template<typename F, typename = typename std::enable_if<!std::is_same<F, A>::value>::type>
A(F&& f) : _f(forward<F>(f)) { }
In this case the implicitly defined copy constructor will be selected, so the destructor doesn't need to check the state of _f
; however if the compiler does not perform copy elision then it (and _f
) will be called twice.
Copy-elision is allowed (12.8p31); the non-elided form must be accessible (12.8p32) but as far as I can tell (and by omission) the compiler is not required to check that it is compilable. So it is permissible for a compiler to either compile or refuse to compile the program; if it does compile, though, it must have performed copy elision.