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

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

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中,我们有了一种新的方法来检查函数是否存在(或者任何非类型成员是否存在),再次依赖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从重载解析中排除序列化(_imp)函数,如果decltype中的第一个表达式无效(即函数不存在)。

void()用于使所有这些函数的返回类型为空。

如果os << obj重载都可用,则使用0参数优先选择os << obj重载(字面量0是int类型,因此第一个重载是更好的匹配)。


现在,您可能需要一个trait来检查函数是否存在。幸运的是,这很容易写出来。不过,请注意,您需要为可能需要的每个不同函数名自己编写trait。

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

生活的例子。

And on to explanations. First, sfinae_true is a helper type, and it basically amounts to the same as writing decltype(void(std::declval<T>().stream(a0)), std::true_type{}). The advantage is simply that it's shorter. Next, the struct has_stream : decltype(...) inherits from either std::true_type or std::false_type in the end, depending on whether the decltype check in test_stream fails or not. Last, std::declval gives you a "value" of whatever type you pass, without you needing to know how you can construct it. Note that this is only possible inside an unevaluated context, such as decltype, sizeof and others.


注意,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的上下文。

其他回答

我在另一个线程中对此写了一个答案(与上面的解决方案不同),也检查继承的成员函数:

SFINAE检查继承的成员函数

以下是该解决方案的一些例子:

例二:

我们正在检查一个具有以下签名的成员: T::const_iterator begin(

template<class T> struct has_const_begin
{
    typedef char (&Yes)[1];
    typedef char (&No)[2];

    template<class U> 
    static Yes test(U const * data, 
                    typename std::enable_if<std::is_same<
                             typename U::const_iterator, 
                             decltype(data->begin())
                    >::value>::type * = 0);
    static No test(...);
    static const bool value = sizeof(Yes) == sizeof(has_const_begin::test((typename std::remove_reference<T>::type*)0));
};

请注意,它甚至检查方法的常量,并且也适用于基本类型。(我的意思是has_const_begin<int>::value为false,不会导致编译时错误。)

示例2

现在我们正在寻找签名:void foo(MyClass&, unsigned)

template<class T> struct has_foo
{
    typedef char (&Yes)[1];
    typedef char (&No)[2];

    template<class U>
    static Yes test(U * data, MyClass* arg1 = 0,
                    typename std::enable_if<std::is_void<
                             decltype(data->foo(*arg1, 1u))
                    >::value>::type * = 0);
    static No test(...);
    static const bool value = sizeof(Yes) == sizeof(has_foo::test((typename std::remove_reference<T>::type*)0));
};

请注意,MyClass不一定是默认可构造的或满足任何特殊的概念。该技术也适用于模板成员。

我急切地等待有关这方面的意见。

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

但是注意,你不能在if分支中调用toString函数。由于编译器将在两个分支中检查有效性,因此在函数不存在的情况下会失败。一种方法是再次使用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.";
}

享受使用它的乐趣。它的优点是它也适用于重载的成员函数,也适用于const成员函数(记得使用std::string(T::*)() const作为成员函数指针类型!)

奇怪的是,竟然没有人建议我在这个网站上看到的下面这个漂亮的把戏:

template <class T>
struct has_foo
{
    struct S { void foo(...); };
    struct derived : S, T {};

    template <typename V, V> struct W {};

    template <typename X>
    char (&test(W<void (X::*)(), &X::foo> *))[1];

    template <typename>
    char (&test(...))[2];

    static const bool value = sizeof(test<derived>(0)) == 1;
};

你必须确保T是一个类。查找foo时的模糊性似乎是替换失败。我让它在gcc上工作,但不确定它是否是标准的。

一个使用SFINAE和模板部分特化的例子,通过编写Has_foo概念检查:

#include <type_traits>
struct A{};

struct B{ int foo(int a, int b);};

struct C{void foo(int a, int b);};

struct D{int foo();};

struct E: public B{};

// available in C++17 onwards as part of <type_traits>
template<typename...>
using void_t = void;

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

template<typename T> 
struct Has_foo<T, void_t<
    std::enable_if_t<
        std::is_same<
            int, 
            decltype(std::declval<T>().foo((int)0, (int)0))
        >::value
    >
>>: std::true_type{};


static_assert(not Has_foo<A>::value, "A does not have a foo");
static_assert(Has_foo<B>::value, "B has a foo");
static_assert(not Has_foo<C>::value, "C has a foo with the wrong return. ");
static_assert(not Has_foo<D>::value, "D has a foo with the wrong arguments. ");
static_assert(Has_foo<E>::value, "E has a foo since it inherits from B");

我知道这个问题已经存在多年了,但我认为对像我这样的人来说,有一个更完整的更新的答案是有用的,它也适用于const重载方法,如std::vector::begin。

基于这个答案和我接下来的问题,这里有一个更完整的答案。注意,这只适用于c++ 11及更高版本。

#include <iostream>
#include <vector>

class EmptyClass{};

template <typename T>
class has_begin
{
    private:
    has_begin() = delete;
    
    struct one { char x[1]; };
    struct two { char x[2]; };

    template <typename C> static one test( decltype(void(std::declval<C &>().begin())) * ) ;
    template <typename C> static two test(...);    

public:
    static constexpr bool value = sizeof(test<T>(0)) == sizeof(one);
};
    
int main(int argc, char *argv[])
{
    std::cout << std::boolalpha;
    std::cout << "vector<int>::begin() exists: " << has_begin<std::vector<int>>::value << std::endl;
    std::cout << "EmptyClass::begin() exists: " << has_begin<EmptyClass>::value << std::endl;
    return 0;
}

或者更短的版本:

#include <iostream>
#include <vector>

class EmptyClass{};

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

template <typename T>
struct has_begin<T, decltype(void(std::declval<T &>().begin()))> : std::true_type {};

int main(int argc, char *argv[])
{
    std::cout << std::boolalpha;
    std::cout << "vector<int>::begin() exists: " << has_begin<std::vector<int>>::value << std::endl;
    std::cout << "EmptyClass exists: " << has_begin<EmptyClass>::value << std::endl;
}

注意,这里必须提供一个完整的示例调用。这意味着如果我们测试resize方法是否存在,那么我们将输入resize(0)。

深魔解释:

这个问题的第一个答案使用了test(decltype(&C::helloworld));然而,当它所测试的方法由于const重载而模棱两可时,这就有问题了,从而导致替换尝试失败。

为了解决这种模糊性,我们使用了一个void语句,它可以接受任何参数,因为它总是被转换为noop,因此模糊性是无效的,并且只要方法存在,调用就有效:

has_begin<T, decltype(void(std::declval<T &>().begin()))>

以下是依次发生的事情: 我们使用std::declval<T &>()来创建一个可调用值,然后可以调用begin。在此之后,begin的值作为参数传递给void语句。然后使用内置decltype检索该void表达式的类型,以便将其用作模板类型参数。如果begin不存在,则替换无效,根据SFINAE,将使用其他声明。