Подтвердить что ты не робот

Неожиданные результаты при использовании std:: is_assignable, boost:: function и nullptr

Следующее выражение, использующее is_assignable, возвращает true при использовании gcc 4.7 и boost 1.49:

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

Однако этот код не скомпилируется:

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

создавая эти сообщения об ошибках:

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

Кроме того, это выражение возвращает false:

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

но этот код компилируется:

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

Результаты is_assignable, похоже, не отражают функциональность boost::function. Я здесь что-то не так? (У меня возникли проблемы с пониманием сообщений об ошибках.)

Я думал, что черты типа должны быть надежным способом определения функциональности классов, используемых в шаблонах. Являются ли свойства типа, представленные на С++ 11, просто несовместимыми с функцией boost:: function?


Чтобы дать этот контекст, я работал над несколькими личными проектами, чтобы лучше ознакомиться с новыми функциями С++ 11. Для этого конкретного проекта я пытаюсь создать класс, который хранит вызываемую функцию, которая может быть "деактивирована". Это примерно то, что я пытаюсь сделать:

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

Для блоков /* func_ is active */ и /* set func_ to deactive */ я хочу предоставить две различные реализации, которые выбираются во время компиляции в зависимости от свойств F. Если nullptr может быть назначен func_ и func_ может использоваться в булевом контексте, тогда я хочу использовать следующее (это то, что выбирается для встроенных указателей функций и 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_;
};

Если nullptr не может быть присвоено func_, тогда я хочу сохранить дополнительное логическое значение в классе, в котором хранится "активный" статус. Эта реализация выбрана для функторов и лямбда-функций:

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

Так как nullptr в настоящее время нельзя назначить boost::function, я бы ожидал, что вторая реализация будет выбрана. Однако, поскольку is_assignable возвращает true для boost::function и nullptr, вместо этого выбирается первая реализация, что приводит к ошибке компиляции в функции deactivate.

4b9b3361

Ответ 1

[Я плохо себя чувствую, когда отвечаю на свой вопрос, но поскольку я так много узнал об этом, я решил, что лучше всего консолидировать эту информацию здесь. Джесси был ОГРОМНОЙ частью, помогающей мне понять все это, поэтому, пожалуйста, воздержитесь от его комментариев выше.]

Итак, почему is_assignable возвращает следующие результаты:

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

несмотря на то, что эти утверждения, похоже, противоречат этим результатам:

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

Первое, что нужно отметить, - это то, что любые черты типа стандартной библиотеки (is_constructible, is_assignable, is_convertible и т.д.) на основе операций проверяют только функцию с допустимым интерфейсом, который соответствует типы, заданные шаблону. В частности, они не проверяют, действительно ли реализация этой функции действительна, когда эти типы подставляются в тело функции.

boost::function не имеет конкретного конструктора для nullptr, но у него есть оператор назначения шаблона "все-все" (вместе с соответствующим конструктором):

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

Это наилучшее соответствие для nullptr, потому что для std::nullptr_t не существует особой перегрузки, и это не требует каких-либо преобразований для другого типа (кроме преобразования в const &). Поскольку подстановка шаблона нашла этот оператор присваивания, std::is_assignable<boost::function<void()>, std::nullptr_t> возвращает true.

Однако в теле этой функции Functor ожидается тип вызываемого типа; то есть f();, как ожидается, будет действительным оператором. nullptr не является вызываемым объектом, поэтому следующий код приводит к ошибке компилятора, которая была указана в вопросе:

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

Но почему std::is_assignable<boost::function<void()>, decltype(NULL)> возвращает false? boost::function не имеет конкретного оператора присваивания для параметра int, так почему же не тот же оператор присваивания шаблонов "все-все", который используется для int и std::nullptr_t?

Раньше я упростил код для этого оператора присваивания, не обращая внимания на аспекты метапрограммирования, но поскольку они теперь актуальны, я добавлю их обратно:

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)

Должно быть достаточно очевидно, что конструкция метапрограммирования enable_if_c используется здесь для предотвращения создания экземпляра этого оператора присваивания, когда тип параметра int (т.е. когда is_integral возвращает true). Таким образом, когда правая сторона оператора присваивания имеет тип int, для boost::function нет соответствующих операторов присваивания. Вот почему std::is_assignable<boost::function<void()>, decltype(NULL)> возвращает false, так как NULL имеет тип int (для GCC по крайней мере).

Но это все еще не объясняет, почему f = NULL; компилируется правильно. Чтобы объяснить это, важно отметить, что значение 0 неявно конвертируется в любой тип указателя. boost::function использует это с помощью оператора присваивания, который принимает указатель на частную структуру. (Ниже приведена значительно упрощенная версия кода из boost::function, но этого достаточно для демонстрации моей точки):

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

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

Так как clear_type является частной структурой, любой внешний код не может создать экземпляр этого объекта. Единственным значением, которое может быть принято этим оператором присваивания, является нулевой указатель, который неявно преобразован из 0. Это оператор присваивания, который вызывается с выражением f = NULL;.


Итак, это объясняет, почему операторы is_assignable и присваивания работают так, как они это делают, но это все равно не помогает мне решить мою первоначальную проблему: как определить, может ли данный тип принимать nullptr или NULL?

К сожалению, я все еще ограничен характеристиками типов из-за их способности обнаруживать, существует ли действительный интерфейс. Для nullptr, похоже, нет хорошего ответа. С boost::function для nullptr существует действительный интерфейс, но реализация этого тела недействительна для этого типа, что всегда вызывает ошибку компилятора для таких операторов, как f = nullptr;.

Но могу ли я правильно определить, что NULL можно присвоить заданному типу, например boost::function, во время компиляции? std::is_assignable требует, чтобы я предоставлял тип второго аргумента. Мы уже знаем, что decltype(NULL) не будет работать, так как это оценивается как int. Я мог бы использовать boost::function<void()>::function::clear_type* как тип, но это очень многословие и требует, чтобы я знал внутренние детали того типа, с которым я работаю.

Элегантное решение включает в себя создание персонализированной черты характера, которая происходит от Люка Дантона в другом сообщении здесь, в формате SO. Я не буду описывать детали этого подхода, поскольку они объясняются намного лучше в другом вопросе, но код для моего пользовательского типа можно увидеть здесь:

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

Я могу использовать эту новую черту типа аналогично std::is_assignable, но мне нужно только указать тип объекта в левой части:

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

Как и все черты типа, это все равно будет проверять только допустимый интерфейс, игнорируя действительность тела функции, но в конечном итоге позволяет мне правильно определить, может ли NULL назначаться boost::function (и любому другому типу) в время компиляции.