是否有可能编写一个模板,根据某个成员函数是否定义在类上而改变行为?

下面是我想写的一个简单的例子:

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

因此,如果类T定义了toString(),那么它就使用它;否则,它就不会。我不知道如何做的神奇部分是“FUNCTION_EXISTS”部分。


当前回答

这是一个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 {};

Trait 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。你向它传递你想要的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++特性。这样做可能是不值得的,因为(代码内联的)好处不值得付出代价(几乎没有人理解它是如何工作的),但是上述解决方案的存在可能会引起人们的兴趣。

其他回答

这是一个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 {};

Trait 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。你向它传递你想要的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++特性。这样做可能是不值得的,因为(代码内联的)好处不值得付出代价(几乎没有人理解它是如何工作的),但是上述解决方案的存在可能会引起人们的兴趣。

我一直在寻找一个方法,允许以某种方式不绑定结构名has_member类的成员的名字。 实际上,如果lambda可以被允许在未求值的表达式中(这是被标准禁止的),这将更简单,即has_member<ClassName, SOME_MACRO_WITH_DECLTYPE(member_name)>

#include <iostream>
#include <list>
#include <type_traits>

#define LAMBDA_FOR_MEMBER_NAME(NAME) [](auto object_instance) -> decltype(&(decltype(object_instance)::NAME)) {}

template<typename T>
struct TypeGetter
{
    constexpr TypeGetter() = default;
    constexpr TypeGetter(T) {}
    using type = T;

    constexpr auto getValue()
    {
        return std::declval<type>();
    }
};

template<typename T, typename LambdaExpressionT>
struct has_member {
    using lambda_prototype = LambdaExpressionT;

    //SFINAE
    template<class ValueT, class = void>
    struct is_void_t_deducable : std::false_type {};

    template<class ValueT>
    struct is_void_t_deducable<ValueT,
        std::void_t<decltype(std::declval<lambda_prototype>()(std::declval<ValueT>()))>> : std::true_type {};

    static constexpr bool value = is_void_t_deducable<T>::value;
};

struct SimpleClass
{
    int field;
    void method() {}
};

int main(void)
{   
    const auto helpful_lambda = LAMBDA_FOR_MEMBER_NAME(field);
    using member_field = decltype(helpful_lambda);
    std::cout << has_member<SimpleClass, member_field>::value;

    const auto lambda = LAMBDA_FOR_MEMBER_NAME(method);
    using member_method = decltype(lambda);
    std::cout << has_member<SimpleClass, member_method>::value;
    
}

虽然这个问题是两年前的事了,但我敢补充我的答案。希望它能澄清之前无可争议的优秀解决方案。我采纳了Nicola Bonelli和Johannes Schaub非常有用的答案,并将它们合并到一个解决方案中,恕我之言,这个解决方案更易于阅读,更清晰,不需要扩展类型:

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检查了它。 这主要归功于尼古拉·博内利和约翰内斯·绍布,如果我的回答对你有帮助,请给他们投票:)

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

你可以跳过c++ 14中所有的元编程,只需要从fit库中使用fit::条件来编写:

template<class T>
std::string optionalToString(T* x)
{
    return fit::conditional(
        [](auto* obj) -> decltype(obj->toString()) { return obj->toString(); },
        [](auto*) { return "toString not defined"; }
    )(x);
}

你也可以直接从lambdas中创建函数:

FIT_STATIC_LAMBDA_FUNCTION(optionalToString) = fit::conditional(
    [](auto* obj) -> decltype(obj->toString(), std::string()) { return obj->toString(); },
    [](auto*) -> std::string { return "toString not defined"; }
);

然而,如果你使用的编译器不支持泛型lambdas,你将不得不编写单独的函数对象:

struct withToString
{
    template<class T>
    auto operator()(T* obj) const -> decltype(obj->toString(), std::string())
    {
        return obj->toString();
    }
};

struct withoutToString
{
    template<class T>
    std::string operator()(T*) const
    {
        return "toString not defined";
    }
};

FIT_STATIC_FUNCTION(optionalToString) = fit::conditional(
    withToString(),
    withoutToString()
);