Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected results when using std::is_assignable, boost::function, and nullptr

Tags:

c++

c++11

boost

The following expression using is_assignable returns true when using gcc 4.7 and boost 1.49:

typedef boost::function<void()> F;
std::is_assignable<F, std::nullptr_t>::value

However, this code fails to compile:

boost::function<void()> f;
f = nullptr;

producing these error messages:

In file included from c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/detail/maybe_include.hpp:13:0,
             from c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/detail/function_iterate.hpp:14,
             from c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/preprocessor/iteration/detail/iter/forward1.hpp:47,
             from c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function.hpp:64,
             from ..\main.cpp:8:
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp: In instantiation of 'static void boost::detail::function::void_function_obj_invoker0<FunctionObj, R>::invoke(boost::detail::function::function_buffer&) [with FunctionObj = std::nullptr_t; R = void]':
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp:907:60:   required from 'void boost::function0<R>::assign_to(Functor) [with Functor = std::nullptr_t; R = void]'
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp:722:7:   required from 'boost::function0<R>::function0(Functor, typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, int>::type) [with Functor = std::nullptr_t; R = void; typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, int>::type = int]'
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp:1042:16:   required from 'boost::function<R()>::function(Functor, typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, int>::type) [with Functor = std::nullptr_t; R = void; typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, int>::type = int]'
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp:1083:5:   required from 'typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, boost::function<R()>&>::type boost::function<R()>::operator=(Functor) [with Functor = std::nullptr_t; R = void; typename boost::enable_if_c<boost::type_traits::ice_not<boost::is_integral<Functor>::value>::value, boost::function<R()>&>::type = boost::function<void()>&]'
..\main.cpp:172:6:   required from here
c:\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.0/../../../../include/boost/function/function_template.hpp:153:11: error: '* f' cannot be used as a function

Additionally, this expression returns false:

typedef boost::function<void()> G;
std::is_assignable<G, decltype(NULL)>::value

but this code does compile:

boost::function<void()> g;
g = NULL;

The results of is_assignable don't seem to properly reflect the functionality of boost::function. Am I doing something wrong here? (I'm having trouble making sense of the error messages.)

I thought the type traits were supposed to be a reliable way of determining the functionality of the classes used in templates. Are the type traits provided in C++11 simply incompatible with boost::function?


To give this some context, I've been working on several personal projects to better familiarize myself with the new features of C++11. For this particular project, I'm attempting to create a class that stores a callable function that can be "deactivated". This is roughly what I'm trying to do:

template <typename F>
class callable_function
{
public:
    callable_function(F func) : func_(func)
    {
        /* func_ is initially active */
    }

    void call()
    {
        if (/* func_ is active */) func_();
    }

    void deactivate()
    {
        /* set func_ to deactive */
    }

private:
    F func_;
};

For the /* func_ is active */ and /* set func_ to deactive */ blocks, I want to provide two different implementations that are selected at compile time depending on the properties of F. If nullptr can be assigned to func_ and func_ can be used in a boolean context, then I want to use the following (which is what gets selected for built-in function pointers and std::function):

template <typename F>
class callable_function
{
public:
    callable_function(F func) : func_(func) {}

    void call()
    {
        if (func_) func_();
    }

    void deactivate()
    {
        func_ = nullptr;
    }

private:
    F func_;
};

If nullptr cannot be assigned to func_, then I want to store an additional boolean value within the class that stores the "active" status. This implementation is selected for functors and lambda functions:

template <typename F>
class callable_function
{
public:
    callable_function(F func) : func_(func), active_(true) {}

    void call()
    {
        if (active_) func_();
    }

    void deactivate()
    {
        active_ = false;
    }

private:
    F func_;
    bool active_;
};

Since nullptr currently cannot be assigned to boost::function, I would expect the second implementation to be chosen. However, since is_assignable is returning true for boost::function and nullptr, the first implementation is selected instead, which results in a compilation error in the deactivate function.

like image 325
Michael Pierce Avatar asked Apr 19 '12 01:04

Michael Pierce


1 Answers

[I feel bad about answering my own question, but since I've learned so much concerning it, I figured it would be best to consolidate that information here. Jesse was a HUGE part of helping me understand all of this, so please upvote his comments above.]

So, why does is_assignable return the following results:

typedef boost::function<void()> F;
std::is_assignable<F, std::nullptr_t>::value // true
std::is_assignable<F, decltype(NULL)>::value // false

despite the fact that these statements seem to contradict those results:

boost::function<void()> f;
f = nullptr; // fails to compile
f = NULL;    // compiles correctly

The first thing to note is that any of the operations-based type traits of the standard library (is_constructible, is_assignable, is_convertible, etc.) only check for a function with a valid interface that matches the types given to the template. In particular, they do not check to see if the implementation of that function is valid when those types are substituted into the function body.

boost::function does not have a specific constructor for nullptr, but it does have a "catch-all" template assignment operator (along with a corresponding constructor):

template<typename Functor>
BOOST_FUNCTION_FUNCTION& operator=(Functor const & f);

This is the best match for nullptr, because there isn't a specific overload for std::nullptr_t and this one doesn't require any conversions to another type (aside from the conversion to a const &). Because the template substitution found this assignment operator, std::is_assignable<boost::function<void()>, std::nullptr_t> returns true.

However, within the body of this function, Functor is expected to be a callable type; that is, f(); is expected to be a valid statement. nullptr is not a callable object, therefore, the following code results in the compiler error that was listed in the question:

boost::function<void()> f;
f = nullptr; // fails to compile

But why does std::is_assignable<boost::function<void()>, decltype(NULL)> return false? boost::function doesn't have a specific assignment operator for an int parameter, so why isn't the same "catch-all" template assignment operator used for int and std::nullptr_t?

Earlier I simplified the code for this assignment operator by leaving out the metaprogramming aspects, but since they are now relevant, I'll add them back:

template<typename Functor>
typename enable_if_c<
           (boost::type_traits::ice_not<
             (is_integral<Functor>::value)>::value),
           BOOST_FUNCTION_FUNCTION&>::type
operator=(Functor const & f)

It should be fairly self-evident that the metaprogramming construct enable_if_c is used here to prevent the instantiation of this assignment operator when the type of the parameter is int (that is, when is_integral returns true). Thus, when the right hand side of an assignment statement is of type int, there are no matching assignment operators for boost::function. This is why std::is_assignable<boost::function<void()>, decltype(NULL)> returns false, since NULL is of type int (for GCC at least).

But this still doesn't explain why f = NULL; compiles correctly. To explain this, it is important to note that the value 0 is implicitly convertible to any pointer type. boost::function exploits this by using an assignment operator that accepts a pointer to a private structure. (The following is a greatly simplified version of the code from boost::function, but it is sufficient for demonstrating my point):

namespace boost
{
    template<typename R()>
    function
    {
    private:
        struct clear_type {}
        //...

    public:
        BOOST_FUNCTION_FUNCTION& operator=(clear_type*);
        //...
    };
}

Since clear_type is a private structure, any external code is unable to create an instance of it. The only value that can be accepted by this assignment operator is a null pointer that was implicitly converted from 0. This is the assignment operator that is called with the expression f = NULL;.


So that explains why the is_assignable and the assignment statements work the way that they do, but it still doesn't help me solve my original problem: how do I detect whether a given type can accept nullptr or NULL?

Unfortunately, I'm still limited with the type traits due to their ability to only detect whether a valid interface exists. For nullptr, there appears to be no good answer. With boost::function, a valid interface does exist for nullptr, but the implementation of the function body is invalid for this type, which will always cause a compiler error for statements such as f = nullptr;.

But can I correctly detect that NULL can be assigned to a given type, such as boost::function, at compile time? std::is_assignable requires that I provide the type of the second argument. We already know that decltype(NULL) won't work, since this evaluates to int. I could use boost::function<void()>::function::clear_type* as the type, but this is very wordy and requires that I know the internal details of the type that I'm working with.

An elegant solution involves creating a custom type trait, which comes from Luc Danton in another post here on SO. I won't describe the details of this approach, as they are explained much better in the other question, but the code for my custom type trait can be seen here:

template<typename> struct Void { typedef void type; };

template<typename T, typename Sfinae = void>
struct is_assignable_with_NULL: std::false_type {};

template<typename T>
struct is_assignable_with_NULL<T, 
    typename Void< decltype( std::declval<T>() = NULL ) >::type
>: std::true_type {};

I can use this new type trait similarly to std::is_assignable, but I only need to provide the type of the object on the left-hand side:

is_assignable_by_NULL<boost::function<void()>::value

Like all type traits, this will still only check for a valid interface, ignoring the validity of the function body, but it finally allows me to correctly determine whether NULL can be assigned to boost::function (and any other type) at compile time.

like image 106
Michael Pierce Avatar answered Nov 05 '22 03:11

Michael Pierce