Можно ли написать шаблон для проверки существования функции? - PullRequest
438 голосов
/ 02 ноября 2008

Можно ли написать шаблон, который меняет поведение в зависимости от того, определена ли определенная функция-член в классе?

Вот простой пример того, что я хотел бы написать:

template<class T>
std::string optionalToString(T* obj)
{
    if (FUNCTION_EXISTS(T->toString))
        return obj->toString();
    else
        return "toString not defined";
}

Итак, если class T определил toString(), то он использует его; в противном случае это не так. Волшебная часть, которую я не знаю, как делать, это часть "FUNCTION_EXISTS".

Ответы [ 25 ]

298 голосов
/ 03 ноября 2008

Да, с SFINAE вы можете проверить, предоставляет ли данный класс определенный метод. Вот рабочий код:

#include <iostream>

struct Hello
{
    int helloworld() { return 0; }
};

struct Generic {};    

// SFINAE test
template <typename T>
class has_helloworld
{
    typedef char one;
    struct two { char x[2]; };

    template <typename C> static one test( typeof(&C::helloworld) ) ;
    template <typename C> static two test(...);    

public:
    enum { value = sizeof(test<T>(0)) == sizeof(char) };
};

int main(int argc, char *argv[])
{
    std::cout << has_helloworld<Hello>::value << std::endl;
    std::cout << has_helloworld<Generic>::value << std::endl;
    return 0;
}

Я только что протестировал его с Linux и gcc 4.1 / 4.3. Я не знаю, переносимо ли это на другие платформы, на которых работают другие компиляторы.

254 голосов
/ 06 февраля 2012

Этот вопрос старый, но в C ++ 11 мы получили новый способ проверки существования функций (или существования любого нетипичного члена, на самом деле), снова полагаясь на SFINAE:

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, int)
    -> decltype(os << obj, void())
{
  os << obj;
}

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, long)
    -> decltype(obj.stream(os), void())
{
  obj.stream(os);
}

template<class T>
auto serialize(std::ostream& os, T const& obj)
    -> decltype(serialize_imp(os, obj, 0), void())
{
  serialize_imp(os, obj, 0);
}

Теперь о некоторых объяснениях. Во-первых, я использую выражение SFINAE , чтобы исключить функции serialize(_imp) из разрешения перегрузки, если первое выражение внутри decltype недопустимо (иначе функция не существует).

void() используется для создания типа возврата всех этих функций void.

Аргумент 0 используется для предпочтения перегрузки os << obj, если доступны оба (литерал 0 имеет тип int, и поэтому первая перегрузка лучше подходит).


Теперь вы, вероятно, хотите, чтобы черта проверяла, существует ли функция. К счастью, это легко написать. Обратите внимание, что вам нужно написать черту себя для каждого имени функции, которое вам может понадобиться.

#include <type_traits>

template<class>
struct sfinae_true : std::true_type{};

namespace detail{
  template<class T, class A0>
  static auto test_stream(int)
      -> sfinae_true<decltype(std::declval<T>().stream(std::declval<A0>()))>;
  template<class, class A0>
  static auto test_stream(long) -> std::false_type;
} // detail::

template<class T, class Arg>
struct has_stream : decltype(detail::test_stream<T, Arg>(0)){};

Живой пример.

И к объяснениям. Во-первых, sfinae_true является вспомогательным типом, и он в основном равен тому, что написано decltype(void(std::declval<T>().stream(a0)), std::true_type{}). Преимущество в том, что оно короче.
Затем struct has_stream : decltype(...) наследуется от std::true_type или std::false_type в конце, в зависимости от того, не прошла проверка decltype в test_stream или нет.
Наконец, std::declval дает вам «значение» любого типа, который вы передаете, без необходимости знать, как вы можете его построить. Обратите внимание, что это возможно только в недооцененном контексте, таком как decltype, sizeof и другие.


Обратите внимание, что decltype необязательно, поскольку sizeof (и все неоцененные контексты) получили это улучшение. Просто decltype уже предоставляет тип и как таковой просто чище. Вот sizeof версия одной из перегрузок:

template<class T>
void serialize_imp(std::ostream& os, T const& obj, int,
    int(*)[sizeof((os << obj),0)] = 0)
{
  os << obj;
}

Параметры int и long все еще существуют по той же причине. Указатель массива используется для предоставления контекста, в котором можно использовать sizeof.

157 голосов
/ 05 ноября 2008

C ++ позволяет использовать для этого SFINAE (обратите внимание, что с функциями C ++ 11 это проще, поскольку он поддерживает расширенный SFINAE для почти произвольных выражений - нижеприведенный код был создан для работы с общим C ++ 03 компиляторов):

#define HAS_MEM_FUNC(func, name)                                        \
    template<typename T, typename Sign>                                 \
    struct name {                                                       \
        typedef char yes[1];                                            \
        typedef char no [2];                                            \
        template <typename U, U> struct type_check;                     \
        template <typename _1> static yes &chk(type_check<Sign, &_1::func > *); \
        template <typename   > static no  &chk(...);                    \
        static bool const value = sizeof(chk<T>(0)) == sizeof(yes);     \
    }

вышеупомянутый шаблон и макрос пытается создать экземпляр шаблона, давая ему тип указателя на функцию-член и фактический указатель на функцию-член. Если типы не подходят, SFINAE вызывает игнорирование шаблона. Использование как это:

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> void
doSomething() {
   if(has_to_string<T, std::string(T::*)()>::value) {
      ...
   } else {
      ...
   }
}

Но учтите, что вы не можете просто вызвать эту функцию toString в этой ветке if. поскольку компилятор проверит правильность в обеих ветвях, это может привести к сбою в случаях, когда функция не существует. Один из способов - использовать SFINAE еще раз (enable_if также можно получить из boost):

template<bool C, typename T = void>
struct enable_if {
  typedef T type;
};

template<typename T>
struct enable_if<false, T> { };

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> 
typename enable_if<has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T has toString ... */
   return t->toString();
}

template<typename T> 
typename enable_if<!has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T doesnt have toString ... */
   return "T::toString() does not exist.";
}

Получайте удовольствие, используя его. Преимущество этого в том, что он также работает для перегруженных функций-членов, а также для константных функций-членов (помните, что std::string(T::*)() const следует использовать в качестве типа указателя на функцию-член!).

54 голосов
/ 02 сентября 2010

Хотя этому вопросу два года, я позволю себе добавить свой ответ. Надеемся, что она будет разъяснено предыдущий, бесспорно отличный, решение. Я взял очень полезные ответы Николая Бонелли и Йоханнеса Шауба и объединил их в решение, которое, ИМХО, более читабельно, понятно и не требует расширения typeof:

template <class Type>
class TypeHasToString
{
    // This type won't compile if the second template parameter isn't of type T,
    // so I can put a function pointer type in the first parameter and the function
    // itself in the second thus checking that the function has a specific signature.
    template <typename T, T> struct TypeCheck;

    typedef char Yes;
    typedef long No;

    // A helper struct to hold the declaration of the function pointer.
    // Change it if the function signature changes.
    template <typename T> struct ToString
    {
        typedef void (T::*fptr)();
    };

    template <typename T> static Yes HasToString(TypeCheck< typename ToString<T>::fptr, &T::toString >*);
    template <typename T> static No  HasToString(...);

public:
    static bool const value = (sizeof(HasToString<Type>(0)) == sizeof(Yes));
};

Я проверил это с помощью gcc 4.1.2. В основном, это заслуга Никола Бонелли и Йоханнеса Шауба, поэтому, если мой ответ поможет вам, проголосуйте за них :)

51 голосов
/ 25 февраля 2014

C ++ 20 - requires выражения

С C ++ 20 приходят концепции и различные инструменты, такие как requires выражения , которые являются встроенным способом проверки существования функции. С помощью tehm вы можете переписать вашу optionalToString функцию следующим образом:

template<class T>
std::string optionalToString(T* obj)
{
    constexpr bool has_toString = requires(const T& t) {
        t.toString();
    };

    if constexpr (has_toString)
        return obj->toString();
    else
        return "toString not defined";
}

Pre-C ++ 20 - Набор инструментов для обнаружения

N4502 предлагает детектирование для включения в стандартную библиотеку C ++ 17, которое может решить проблему несколько элегантным способом. Более того, он только что был принят в основы библиотеки TS v2. В нем представлены некоторые метафункции, в том числе std::is_detected, которые можно использовать для простой записи метафункций обнаружения типов или функций поверх них. Вот как это можно использовать:

template<typename T>
using toString_t = decltype( std::declval<T&>().toString() );

template<typename T>
constexpr bool has_toString = std::is_detected_v<toString_t, T>;

Обратите внимание, что приведенный выше пример не проверен. Набор инструментов для обнаружения еще не доступен в стандартных библиотеках, но в предложении содержится полная реализация, которую вы можете легко скопировать, если вам это действительно нужно. Он прекрасно работает с функцией C ++ 17 if constexpr:

template<class T>
std::string optionalToString(T* obj)
{
    if constexpr (has_toString<T>)
        return obj->toString();
    else
        return "toString not defined";
}

Boost.TTI

Еще одним идиоматическим набором инструментов для выполнения такой проверки, хотя и менее элегантным, является Boost.TTI , представленный в Boost 1.54.0. Для вашего примера вы должны использовать макрос BOOST_TTI_HAS_MEMBER_FUNCTION. Вот как это можно использовать:

#include <boost/tti/has_member_function.hpp>

// Generate the metafunction
BOOST_TTI_HAS_MEMBER_FUNCTION(toString)

// Check whether T has a member function toString
// which takes no parameter and returns a std::string
constexpr bool foo = has_member_function_toString<T, std::string>::value;

Затем вы можете использовать bool для создания проверки SFINAE.

Объяснение

Макрос BOOST_TTI_HAS_MEMBER_FUNCTION генерирует метафункцию has_member_function_toString, которая принимает проверенный тип в качестве первого параметра шаблона. Второй параметр шаблона соответствует типу возвращаемого значения функции-члена, а следующие параметры соответствуют типам параметров функции. Член value содержит true, если класс T имеет функцию-член std::string toString().

В качестве альтернативы, has_member_function_toString может принимать указатель на функцию-член в качестве параметра шаблона. Следовательно, можно заменить has_member_function_toString<T, std::string>::value на has_member_function_toString<std::string T::* ()>::value.

29 голосов
/ 06 августа 2015

Простое решение для C ++ 11:

template<class T>
auto optionalToString(T* obj)
 -> decltype(  obj->toString()  )
{
    return     obj->toString();
}
auto optionalToString(...) -> string
{
    return "toString not defined";
}

Обновление, 3 года спустя: (и это не проверено). Чтобы проверить на существование, я думаю, что это будет работать:

template<class T>
constexpr auto test_has_toString_method(T* obj)
 -> decltype(  obj->toString() , std::true_type{} )
{
    return     obj->toString();
}
constexpr auto test_has_toString_method(...) -> std::false_type
{
    return "toString not defined";
}
29 голосов
/ 02 ноября 2008

Это то, для чего существуют черты типа. К сожалению, они должны быть определены вручную. В вашем случае представьте следующее:

template <typename T>
struct response_trait {
    static bool const has_tostring = false;
};

template <>
struct response_trait<your_type_with_tostring> {
    static bool const has_tostring = true;
}
21 голосов
/ 15 июня 2015

Ну, на этот вопрос уже есть длинный список ответов, но я хотел бы подчеркнуть комментарий Морвенна: есть предложение для C ++ 17, которое делает его действительно намного проще. Подробнее см. N4502 , но в качестве отдельного примера рассмотрите следующее.

Эта часть является постоянной, поместите ее в заголовок.

// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4502.pdf.
template <typename...>
using void_t = void;

// Primary template handles all types not supporting the operation.
template <typename, template <typename> class, typename = void_t<>>
struct detect : std::false_type {};

// Specialization recognizes/validates only types supporting the archetype.
template <typename T, template <typename> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};

затем есть переменная часть, где вы указываете, что вы ищете (тип, тип члена, функцию, функцию-член и т. Д.). В случае ОП:

template <typename T>
using toString_t = decltype(std::declval<T>().toString());

template <typename T>
using has_toString = detect<T, toString_t>;

В следующем примере, взятом из N4502 , показан более сложный зонд:

// Archetypal expression for assignment operation.
template <typename T>
using assign_t = decltype(std::declval<T&>() = std::declval<T const &>())

// Trait corresponding to that archetype.
template <typename T>
using is_assignable = detect<T, assign_t>;

По сравнению с другими реализациями, описанными выше, эта довольно проста: достаточно сокращенного набора инструментов (void_t и detect), нет необходимости в волосатых макросах. Кроме того, было сообщено (см. N4502 ), что он значительно более эффективен (время компиляции и потребление памяти компилятором), чем предыдущие подходы.

Вот живой пример . Он отлично работает с Clang, но, к сожалению, версии GCC до 5.1 следовали другой интерпретации стандарта C ++ 11, из-за которой void_t не работал должным образом. Yakk уже предоставил обходной путь: используйте следующее определение void_t ( void_t в списке параметров работает, но не как тип возвращаемого значения ):

#if __GNUC__ < 5 && ! defined __clang__
// https://stackoverflow.com/a/28967049/1353549
template <typename...>
struct voider
{
  using type = void;
};
template <typename...Ts>
using void_t = typename voider<Ts...>::type;
#else
template <typename...>
using void_t = void;
#endif
10 голосов
/ 02 июня 2014

Это решение C ++ 11 для общей проблемы, если «Если бы я сделал X, он бы скомпилировал?»

template<class> struct type_sink { typedef void type; }; // consumes a type, and makes it `void`
template<class T> using type_sink_t = typename type_sink<T>::type;
template<class T, class=void> struct has_to_string : std::false_type {}; \
template<class T> struct has_to_string<
  T,
  type_sink_t< decltype( std::declval<T>().toString() ) >
>: std::true_type {};

Черта has_to_string такая, что has_to_string<T>::value равно true тогда и только тогда, когда T имеет метод .toString, который может быть вызван с 0 аргументами в этом контексте.

Далее я бы использовал диспетчеризацию тегов:

namespace details {
  template<class T>
  std::string optionalToString_helper(T* obj, std::true_type /*has_to_string*/) {
    return obj->toString();
  }
  template<class T>
  std::string optionalToString_helper(T* obj, std::false_type /*has_to_string*/) {
    return "toString not defined";
  }
}
template<class T>
std::string optionalToString(T* obj) {
  return details::optionalToString_helper( obj, has_to_string<T>{} );
}

, который, как правило, более удобен в обслуживании, чем сложные выражения SFINAE.

Вы можете написать эти черты с помощью макроса, если вы обнаружите, что делаете это много, но они относительно просты (несколько строк каждая), поэтому, возможно, не стоит:

#define MAKE_CODE_TRAIT( TRAIT_NAME, ... ) \
template<class T, class=void> struct TRAIT_NAME : std::false_type {}; \
template<class T> struct TRAIT_NAME< T, type_sink_t< decltype( __VA_ARGS__ ) > >: std::true_type {};

вышеописанное создает макрос MAKE_CODE_TRAIT. Вы передаете ему имя желаемой черты и некоторый код, который может проверять тип T. Таким образом:

MAKE_CODE_TRAIT( has_to_string, std::declval<T>().toString() )

создает вышеупомянутый класс черт.

Кроме того, вышеприведенная методика является частью того, что MS называет «выражением SFINAE», и их компилятор 2013 года выходит из строя довольно сильно.

Обратите внимание, что в C ++ 1y возможен следующий синтаксис:

template<class T>
std::string optionalToString(T* obj) {
  return compiled_if< has_to_string >(*obj, [&](auto&& obj) {
    return obj.toString();
  }) *compiled_else ([&]{ 
    return "toString not defined";
  });
}

- это условная ветвь встроенной компиляции, которая использует множество функций C ++. Делать это, вероятно, не стоит, так как выгода (от встроенного кода) не стоит затрат (почти никто не понимает, как это работает), но может оказаться интересным наличие вышеупомянутого решения.

9 голосов
/ 13 июня 2011

Вот некоторые фрагменты использования: * Храбрость для всего этого дальше вниз

Проверка для члена x в данном классе. Может быть var, func, class, union или enum:

CREATE_MEMBER_CHECK(x);
bool has_x = has_member_x<class_to_check_for_x>::value;

Проверка функции члена void x():

//Func signature MUST have T as template variable here... simpler this way :\
CREATE_MEMBER_FUNC_SIG_CHECK(x, void (T::*)(), void__x);
bool has_func_sig_void__x = has_member_func_void__x<class_to_check_for_x>::value;

Проверка переменной элемента x:

CREATE_MEMBER_VAR_CHECK(x);
bool has_var_x = has_member_var_x<class_to_check_for_x>::value;

Проверка класса участника x:

CREATE_MEMBER_CLASS_CHECK(x);
bool has_class_x = has_member_class_x<class_to_check_for_x>::value;

Проверка членского союза x:

CREATE_MEMBER_UNION_CHECK(x);
bool has_union_x = has_member_union_x<class_to_check_for_x>::value;

Проверка перечисления членов x:

CREATE_MEMBER_ENUM_CHECK(x);
bool has_enum_x = has_member_enum_x<class_to_check_for_x>::value;

Проверка любой функции-члена x независимо от подписи:

CREATE_MEMBER_CHECK(x);
CREATE_MEMBER_VAR_CHECK(x);
CREATE_MEMBER_CLASS_CHECK(x);
CREATE_MEMBER_UNION_CHECK(x);
CREATE_MEMBER_ENUM_CHECK(x);
CREATE_MEMBER_FUNC_CHECK(x);
bool has_any_func_x = has_member_func_x<class_to_check_for_x>::value;

OR

CREATE_MEMBER_CHECKS(x);  //Just stamps out the same macro calls as above.
bool has_any_func_x = has_member_func_x<class_to_check_for_x>::value;

Детали и ядро:

/*
    - Multiple inheritance forces ambiguity of member names.
    - SFINAE is used to make aliases to member names.
    - Expression SFINAE is used in just one generic has_member that can accept
      any alias we pass it.
*/

//Variadic to force ambiguity of class members.  C++11 and up.
template <typename... Args> struct ambiguate : public Args... {};

//Non-variadic version of the line above.
//template <typename A, typename B> struct ambiguate : public A, public B {};

template<typename A, typename = void>
struct got_type : std::false_type {};

template<typename A>
struct got_type<A> : std::true_type {
    typedef A type;
};

template<typename T, T>
struct sig_check : std::true_type {};

template<typename Alias, typename AmbiguitySeed>
struct has_member {
    template<typename C> static char ((&f(decltype(&C::value))))[1];
    template<typename C> static char ((&f(...)))[2];

    //Make sure the member name is consistently spelled the same.
    static_assert(
        (sizeof(f<AmbiguitySeed>(0)) == 1)
        , "Member name specified in AmbiguitySeed is different from member name specified in Alias, or wrong Alias/AmbiguitySeed has been specified."
    );

    static bool const value = sizeof(f<Alias>(0)) == 2;
};

Макросы (El Diablo!):

CREATE_MEMBER_CHECK:

//Check for any member with given name, whether var, func, class, union, enum.
#define CREATE_MEMBER_CHECK(member)                                         \
                                                                            \
template<typename T, typename = std::true_type>                             \
struct Alias_##member;                                                      \
                                                                            \
template<typename T>                                                        \
struct Alias_##member <                                                     \
    T, std::integral_constant<bool, got_type<decltype(&T::member)>::value>  \
> { static const decltype(&T::member) value; };                             \
                                                                            \
struct AmbiguitySeed_##member { char member; };                             \
                                                                            \
template<typename T>                                                        \
struct has_member_##member {                                                \
    static const bool value                                                 \
        = has_member<                                                       \
            Alias_##member<ambiguate<T, AmbiguitySeed_##member>>            \
            , Alias_##member<AmbiguitySeed_##member>                        \
        >::value                                                            \
    ;                                                                       \
}

CREATE_MEMBER_VAR_CHECK:

//Check for member variable with given name.
#define CREATE_MEMBER_VAR_CHECK(var_name)                                   \
                                                                            \
template<typename T, typename = std::true_type>                             \
struct has_member_var_##var_name : std::false_type {};                      \
                                                                            \
template<typename T>                                                        \
struct has_member_var_##var_name<                                           \
    T                                                                       \
    , std::integral_constant<                                               \
        bool                                                                \
        , !std::is_member_function_pointer<decltype(&T::var_name)>::value   \
    >                                                                       \
> : std::true_type {}

CREATE_MEMBER_FUNC_SIG_CHECK:

//Check for member function with given name AND signature.
#define CREATE_MEMBER_FUNC_SIG_CHECK(func_name, func_sig, templ_postfix)    \
                                                                            \
template<typename T, typename = std::true_type>                             \
struct has_member_func_##templ_postfix : std::false_type {};                \
                                                                            \
template<typename T>                                                        \
struct has_member_func_##templ_postfix<                                     \
    T, std::integral_constant<                                              \
        bool                                                                \
        , sig_check<func_sig, &T::func_name>::value                         \
    >                                                                       \
> : std::true_type {}

CREATE_MEMBER_CLASS_CHECK:

//Check for member class with given name.
#define CREATE_MEMBER_CLASS_CHECK(class_name)               \
                                                            \
template<typename T, typename = std::true_type>             \
struct has_member_class_##class_name : std::false_type {};  \
                                                            \
template<typename T>                                        \
struct has_member_class_##class_name<                       \
    T                                                       \
    , std::integral_constant<                               \
        bool                                                \
        , std::is_class<                                    \
            typename got_type<typename T::class_name>::type \
        >::value                                            \
    >                                                       \
> : std::true_type {}

CREATE_MEMBER_UNION_CHECK:

//Check for member union with given name.
#define CREATE_MEMBER_UNION_CHECK(union_name)               \
                                                            \
template<typename T, typename = std::true_type>             \
struct has_member_union_##union_name : std::false_type {};  \
                                                            \
template<typename T>                                        \
struct has_member_union_##union_name<                       \
    T                                                       \
    , std::integral_constant<                               \
        bool                                                \
        , std::is_union<                                    \
            typename got_type<typename T::union_name>::type \
        >::value                                            \
    >                                                       \
> : std::true_type {}

CREATE_MEMBER_ENUM_CHECK:

//Check for member enum with given name.
#define CREATE_MEMBER_ENUM_CHECK(enum_name)                 \
                                                            \
template<typename T, typename = std::true_type>             \
struct has_member_enum_##enum_name : std::false_type {};    \
                                                            \
template<typename T>                                        \
struct has_member_enum_##enum_name<                         \
    T                                                       \
    , std::integral_constant<                               \
        bool                                                \
        , std::is_enum<                                     \
            typename got_type<typename T::enum_name>::type  \
        >::value                                            \
    >                                                       \
> : std::true_type {}

CREATE_MEMBER_FUNC_CHECK:

//Check for function with given name, any signature.
#define CREATE_MEMBER_FUNC_CHECK(func)          \
template<typename T>                            \
struct has_member_func_##func {                 \
    static const bool value                     \
        = has_member_##func<T>::value           \
        && !has_member_var_##func<T>::value     \
        && !has_member_class_##func<T>::value   \
        && !has_member_union_##func<T>::value   \
        && !has_member_enum_##func<T>::value    \
    ;                                           \
}

CREATE_MEMBER_CHECKS:

//Create all the checks for one member.  Does NOT include func sig checks.
#define CREATE_MEMBER_CHECKS(member)    \
CREATE_MEMBER_CHECK(member);            \
CREATE_MEMBER_VAR_CHECK(member);        \
CREATE_MEMBER_CLASS_CHECK(member);      \
CREATE_MEMBER_UNION_CHECK(member);      \
CREATE_MEMBER_ENUM_CHECK(member);       \
CREATE_MEMBER_FUNC_CHECK(member)
...