Ответ 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
(и любому другому типу) в время компиляции.