Should std::function assignment ignore return type

2020-02-14 01:10发布

问题:

Is the code below valid C++ according to the C++11 or C++14 standard?

#include <functional>

int ReturnInt()
{
  return 5;
}

int main( int argc, char **argv )
{
  std::function< void () > BoundType = ReturnInt;
  return 0;
}

The code compiles fine with the latest cygwin versions of gcc (4.8.3) and clang (4.3.2) but not with Visual Studio 2013, Visual Studio November 2013 CTP or the Visual Studio 14 preview. It also compiles on all platforms if std::function is changed to boost::function.

I found this other stack overflow question that suggests it should work.

回答1:

The code is undefined behavior in C++11, and ill-formed in C++14. C++14 adds this Remark to the specification of this constructor:

Remarks: These constructors shall not participate in overload resolution unless f is Callable (20.9.11.2) for argument types ArgTypes... and return type R.

Callable is defined in [func.wrap.func]/p2:

A callable object f of type F is Callable for argument types ArgTypes and return type R if the expression INVOKE (f, declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.9.2).

For this INVOKE to be well formed, the return type of INVOKE without the R must be implicitly convertible to R ([func.require]/p2).

In C++11 these statements were under a Requries clause, which means it is up to the client to get them right, and if the client fails, anything can happen, including successful compilation.

This was changed by LWG 2132.



回答2:

std::function in C++11 and 14 does not have the behavior you desire.

It also fails to SFINAE detect bad overloads.

We can wrap it in a different type that both has the behavior you desire (void discarding return) and has SFINAE bad overload detection while we are at it as follows:

template<class Sig>
struct checked_function;

template<class R, class... Args>
struct checked_function<R(Args...)>:std::function<R(Args...)> {
  using function = std::function<R(Args...)>;
  checked_function(std::nullptr_t):function() {}
  checked_function():function() {}
  template<class F, class=typename std::enable_if<
    std::is_convertible<
      typename std::result_of< F(Args...) >::type
      , R
    >::value
  >::type>
  checked_function( F&& f ):function( std::forward<F>(f) ) {}

  template<class F, class=typename std::enable_if<
    std::is_convertible<
      typename std::result_of< F(Args...) >::type
      , R
    >::value
  >::type>
  checked_function& operator=( F&& f ) { return function::operator=( std::forward<F>(f) ); }

  checked_function& operator=( checked_function const& o ) = default;
  checked_function& operator=( checked_function && o ) = default;
  checked_function( checked_function const& o ) = default;
  checked_function( checked_function && o ) = default;
};

template<class... Args>
struct checked_function<void(Args...)>:std::function<void(Args...)> {
  using function = std::function<void(Args...)>;
  checked_function(std::nullptr_t):function() {}
  checked_function():function() {}
  template<class F, class=typename std::enable_if<
    std::is_same<
      typename std::result_of< F(Args...) >::type
      , void
    >::value
  >::type>
  checked_function( F&& f, int*unused=nullptr ):function( std::forward<F>(f) ) {}

  template<class F>
  static auto wrap(F&& f){
    return [f_=std::forward<F>(f)](auto&&...args){
      f_( std::forward<decltype(args)>(args)... );
    };
  }
  template<class F, class=typename std::enable_if<
    !std::is_same<
      typename std::result_of< F(Args...) >::type
      , void
    >::value
  >::type>
  checked_function( F&& f, void*unused=nullptr ):
    function( wrap(std::forward<F>(f)) ) {}

   template<class F>
  typename std::enable_if<
    !std::is_same<
      typename std::result_of< F(Args...) >::type
      , void
    >::value,
    checked_function&
  >::type operator=( F&& f ) { return function::operator=( wrap(std::forward<F>(f)) ); }

  template<class F>
  typename std::enable_if<
    std::is_same<
      typename std::result_of< F(Args...) >::type
      , void
    >::value,
    checked_function&
  >::type operator=( F&& f ) { return function::operator=( std::forward<F>(f) ); }

  checked_function& operator=( checked_function const& o ) = default;
  checked_function& operator=( checked_function && o ) = default;
  checked_function( checked_function const& o ) = default;
  checked_function( checked_function && o ) = default;
};

It now compiles in C++14 (not in C++11, due to wrap: wrap can be replaced at point of call with a copy of its own body, so...). Could probably reduce boilerplate by a bunch.

It uses some C++14 features (move-into-lambda to be precise in wrap -- you can do away with that by adding more boilerplate).

Not run yet.