Using std::enable_if with out-of-line member functions and templated static member conditions
Asked Answered
P

1

6

I want to use SFINAE to create a templated member function which takes a Consumer functor. Whether something is a consumer depends on a templated static constexpr bool isConsumer member variable. I have simplified my code down to the following example:

#include <type_traits>

template <typename T>
struct Container {
    T data[100];

    template <typename Consumer>
    static constexpr bool isConsumer = std::is_invocable_r_v<void, Consumer, T>;

    template <typename Consumer, std::enable_if_t<isConsumer<Consumer>, int> = 0>
    void forEach(const Consumer &consumer);
};

template <typename T>
template <typename Consumer, std::enable_if_t<Container<T>::template isConsumer<Consumer>, int>>
void Container<T>::forEach(const Consumer &consumer)
{
    for (int i = 0; i < 100; ++i) {
        consumer(data[i]);
    }
}

This does not compile for reasons that I don't comprehend:

<source>:16:20: error: out-of-line definition of 'forEach' does not match any declaration in 'Container<T>'

void Container<T>::forEach(const Consumer &consumer)

                   ^~~~~~~

It does compile just fine when I inline isConsumer, as in, use std::is_invocable_r_v directly. I would like to avoid this because in my real code the signature of the Consumer is quite complicated and this requires quite a bit of copy/paste.

Pulling isConsumer outside the class is not an option either, because in my real code it depends on private typedefs inside Container. It must be inside the class.

How do I use std::enable_if correctly here?

Pecksniffian answered 28/8, 2020 at 22:15 Comment(0)
M
6

It seems that there really is no way to make an out-of-line definition given the current declaration (gcc complains that the declaration uses an "anonymous type")

Possible workarounds:

Use a static_assert instead of SFINAE:

#include <type_traits>

template <typename T>
struct Container {
    template <typename Consumer>
    static constexpr bool isConsumer = /* ... */;

    template <typename Consumer>
    void forEach(const Consumer &consumer);
};

template <typename T>
template <typename Consumer>
void Container<T>::forEach(const Consumer &consumer)
{
    static_assert(isConsumer<Consumer>);
    // ...
}

Fully qualify the declaration too so the definition so they should refer to the same type (This doesn't work in clang. Seems like a clang bug):

#include <type_traits>

template <typename T>
struct Container {
    template <typename Consumer>
    static constexpr bool isConsumer = /* ... */;

    template <typename Consumer, std::enable_if_t<Container<T>::template isConsumer<Consumer>, int> = 0>
    void forEach(const Consumer &consumer);
};

template <typename T>
template <typename Consumer, std::enable_if_t<Container<T>::template isConsumer<Consumer>, int>>
void Container<T>::forEach(const Consumer &consumer)
{
    // ...
}

Delegate to a private function with a small forwarding inner function:

#include <type_traits>

template <typename T>
struct Container {
    template <typename Consumer>
    static constexpr bool isConsumer = /* ... */;

    template <typename Consumer, std::enable_if_t<isConsumer<Consumer>, int> = 0>
    void forEach(const Consumer &consumer) {
        forEachImpl(consumer);
    }
private:
    template<typename Consumer>
    void forEachImpl(const Consumer&);
};

template<typename T>
template<typename Consumer>
void Container<T>::forEachImpl(const Consumer& consumer) {
    // ...
}

Use return type SFINAE instead so you are in the class's namespace for lookup:

#include <type_traits>

template <typename T>
struct Container {
    template <typename Consumer>
    static constexpr bool isConsumer = /* ... */;

    template <typename Consumer>
    std::enable_if_t<isConsumer<Consumer>> forEach(const Consumer &consumer);
};

template <typename T>
template <typename Consumer>
auto Container<T>::forEach(const Consumer &consumer) -> std::enable_if_t<isConsumer<Consumer>>
{
    // ...
}

And of course, just defining it inline.

Martinmartina answered 28/8, 2020 at 22:45 Comment(1)
The second solution you posted only compiles with GCC, but fails with the same error as the original code using clang. I've already tried with static_assert, but then I get error messages inside of my container code instead of at the call site, which is irritating. The third solution looks good though.Pecksniffian

© 2022 - 2024 — McMap. All rights reserved.