SFINAE for detecting existence of non-member template function
Asked Answered
G

1

6

TL;DR I want to write a template function Process(T value) that behaves differently for different values depending on the existence of a non-member function CreateProcessor<T>(). What can I do for that?

I have a problem with SFINAE. Suppose we need to support function CreateProcessor that returns an implementation of interface IProcessor<T> for some type type T.

In C++ we can't create several overloads of a function that differ only in return type, so we have to make function CreateProcessor also be template function parametrized by T.

Now suppose that we want to write a template function Process<T>(T value) that works differently depending on existence of CreateProcessor<T>(), namely it should process value using the processor in case CreateProcessor<T>() is implemented, otherwise it should result in error.

I attempted to write the following code:

#include <cstdio>
#include <type_traits>

// A workaround for void_t as described here: http://en.cppreference.com/w/cpp/types/void_t.
template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

// An interface for a processor that receives a value of specific type.
template<class T>
class IProcessor {
public:
    virtual void process(T value) = 0;
};

// A processor for int.
class IntProcessor : public IProcessor<int> {
public:
    virtual void process(int value) override {
        printf("IntProcessor::process is called for value = %d\n", value);
    }
};

// Template prototype.
template<class T> 
IProcessor<T>* CreateProcessor();

// Template specialization for int.
template<>
IProcessor<int>* CreateProcessor() {
    return new IntProcessor();
}

// Detector of CreateProcessor.
template<class, class=void>
struct CreateProcessorImplemented : std::false_type { };

template<class T>
struct CreateProcessorImplemented<T, void_t<decltype(CreateProcessor<T>())>> : std::true_type { };


// Specializations depending on existence of CreateProcessor.
template <typename T>
typename std::enable_if<CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    IProcessor<T>* processor = CreateProcessor<T>();
    processor->process(value);
}

template <typename T>
typename std::enable_if<!CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    printf("Processor for requested typename is unavailable\n");
}


int main() {
    Process(42);
    Process("abc");

// static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
/* This static_assert fails with an error:
 * code.cpp:56:5: error: static assertion failed: :(
 *      static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
 */
}

Though this results in linkage error:

/tmp/ccTQRc9N.o:code.cpp:function std::enable_if<CreateProcessorImplemented<char const*, void>::value, void>::type Process<char const*>(char const*): error: undefined reference to 'IProcessor<char const*>* CreateProcessor<char const*>()'
collect2: error: ld returned 1 exit status

My idea is that when we resolve CreateProcessorImplemented<char const*>, decltype(CreateProcessor<const char*>()) doesn't fail because there is a template prototype IProcessor<T> CreateProcessor() and compiler considers the decltype to be equal to IProcessor<T> that is somehow logical but not what I need.

Grippe answered 11/3, 2016 at 12:5 Comment(3)
CreateProcessorImplemented always derives from std::true_type because a function doesn't need to have an implementation for decltype to tell you the return type (see std::declval).Eirena
@Simple, That makes sence. I believe there is no way to check if function has an implementation during the compile-time since it requires a knowledge from linker, right?Grippe
An alternative is in the answer from @WojciechFrohmberg. You use a struct with a static member function instead of a non-member function. You can detect if there is no static member function due to the struct not being specialised. It's basically a type trait.Eirena
T
5

One way to make it work is to use wrapper struct to function CreateProcessor like this:

#include <cstdio>
#include <type_traits>

// A workaround for void_t as described here: http://en.cppreference.com/w/cpp/types/void_t.
template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

// An interface for a processor that receives a value of specific type.
template<class T>
class IProcessor {
public:
    virtual void process(T value) = 0;
};

// A processor for int.
class IntProcessor : public IProcessor<int> {
public:
    virtual void process(int value) override {
        printf("IntProcessor::process is called for value = %d\n", value);
    }
};

// Template prototype.
template<class T>
struct ProcessorCreator: std::false_type { 
   static IProcessor<T>* CreateProcessor();
};

// Template specialization for int.
template<>
struct ProcessorCreator<int>: std::true_type {
static IProcessor<int>* CreateProcessor() {
    return new IntProcessor();
}
};

// Detector of CreateProcessor.
template<class, class=void>
struct CreateProcessorImplemented : std::false_type { };

template<class T>
struct CreateProcessorImplemented<T, typename std::enable_if<ProcessorCreator<T>::value>::type > : std::true_type { };


// Specializations depending on existence of CreateProcessor.
template <typename T>
typename std::enable_if<CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    IProcessor<T>* processor = ProcessorCreator<T>::CreateProcessor();
    processor->process(value);
}

template <typename T>
typename std::enable_if<!CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    printf("Processor for requested typename is unavailable\n");
}


int main() {
    Process(42);
    Process("abc");

// static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
/* This static_assert fails with an error:
 * code.cpp:56:5: error: static assertion failed: :(
 *      static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
 */
}

Alternatively you could remove template declaration and pass the IProcessor template parameter type using function overloadings -- by creating dummy argument:

#include <cstdio>
#include <type_traits>

// A workaround for void_t as described here: http://en.cppreference.com/w/cpp/types/void_t.
template<typename... Ts> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

// An interface for a processor that receives a value of specific type.
template<class T>
class IProcessor {
public:
    virtual void process(T value) = 0;
};

// A processor for int.
class IntProcessor : public IProcessor<int> {
public:
    virtual void process(int value) override {
        printf("IntProcessor::process is called for value = %d\n", value);
    }
};


IProcessor<int>* CreateProcessor(const int&) {
    return new IntProcessor();
}

// Detector of CreateProcessor.
template<class, class=void>
struct CreateProcessorImplemented : std::false_type { };

template<class T>
struct CreateProcessorImplemented<T, void_t<decltype(CreateProcessor(std::declval<T>()))>> : std::true_type { };


// Specializations depending on existence of CreateProcessor.
template <typename T>
typename std::enable_if<CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    IProcessor<T>* processor = CreateProcessor(value);
    processor->process(value);
}

template <typename T>
typename std::enable_if<!CreateProcessorImplemented<T>::value, void>::type Process(T value) {
    printf("Processor for requested typename is unavailable\n");
}


int main() {
    Process(42);
    Process("abc");

// static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
/* This static_assert fails with an error:
 * code.cpp:56:5: error: static assertion failed: :(
 *      static_assert(!CreateProcessorImplemented<char const*>::value, ":(");
 */
}
Tripartite answered 11/3, 2016 at 12:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.