Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type trait to check whether a function can compile with a given type

I have two types, CustomNotCompatibleWithRun, CustomCompatibleWithRun, and a run() function:

#include <iostream>
#include <type_traits>

template <typename T_>
struct CustomNotCompatibleWithRun { using T = T_; };

template <typename T_>
struct CustomCompatibleWithRun {
    using T = T_; 
    
    CustomCompatibleWithRun operator+(const CustomCompatibleWithRun& other)
    {
        CustomCompatibleWithRun res;
        res.a = a + other.a;
        res.b = b + other.b;
        return res;
    }
    
    void print() const
    {
        std::cout << "a, b = " << a << ", " << b << "\n";
    }
    
    T a;
    T b;
};

template <typename T>
T run(T a, T b) { return a+b; }

I want to write a type trait, IsTypeCompatibleWithRun, which checks whether a given Type (whose only contract is to have a type alias Type::T) can be used in the run() function. That is:

int main() {
    using T = float;
    CustomCompatibleWithRun<T> obj1{.a = 1.f, .b = 2.f};
    CustomCompatibleWithRun<T> obj2{.a = 2.f, .b = 3.f};
    obj1.print(); // a, b = 1, 2
    obj2.print(); // a, b = 2, 3
    const auto obj3 = obj1 + obj2;
    obj3.print(); // a, b = 3, 5
    const auto obj4 = run(obj1, obj2);
    obj4.print(); // a, b = 3, 5
    CustomNotCompatibleWithRun<T> obj5{};
    CustomNotCompatibleWithRun<T> obj6{};
    // const auto obj7 = obj5 + obj6; // does not compile
    
    std::cout << "IsTypeCompatibleWithRun<CustomCompatibleWithRun>::value: " << IsTypeCompatibleWithRun<CustomCompatibleWithRun<T>>::value << "\n"; // should print 1
    std::cout << "IsTypeCompatibleWithRun<CustomNotCompatibleWithRun>::value: " << IsTypeCompatibleWithRun<CustomNotCompatibleWithRun<T>>::value << "\n"; // should print 0
}

I have tried the three options below:

template <typename Type, typename = void>
struct IsTypeCompatibleWithRun : std::false_type {};

// Option 1
template <typename Type>
struct IsTypeCompatibleWithRun<Type, decltype(void(run<typename Type::T>(std::declval<typename Type::T>(), std::declval<typename Type::T>())))> : std::true_type {};

// Option 2
template <typename Type>
struct IsTypeCompatibleWithRun<Type, decltype(void(std::declval<Type>() + std::declval<Type>()))> : std::true_type {};

// Option 3
template <typename Type>
struct IsTypeCompatibleWithRun<Type, std::void_t<decltype(run(std::declval<Type>(), std::declval<Type>()))>> : std::true_type {};

And only Option 2 works - from what I understand that is because we force the + operator inside the type trait itself, whereas for Options 1 and 3, only the signature of the run() function is checked? However I'm dissatisfied with Option 2 because it requires repeating the actual function body of run() into the type trait itself. In this simple example it is fine but in my application the body of the run() function is much more complicated and it is impossible to repeat it inside the type trait.

I then tried changing the signature of run() to

template <typename T>
auto run(T a, T b)

but the code would not even compile anymore. I then tried adding a trailing return type

template <typename T>
auto run(T a, T b) -> decltype(a+b)

and now Option 3 would work, but not Option 1. Why is that?

Either way, the above would involve having to repeat the a+b part in the trailing return type so it is not a solution for me.

Is there a way to achieve this type trait without altering run() and solely relying on run() (and not repeating its function body), in C++ 14?

like image 216
zhanginou Avatar asked Sep 15 '25 12:09

zhanginou


2 Answers

What you're seeing is a limitation of SFINAE (Substitution Failure Is Not An Error) in C++14. This only checks the function signature during template substitution, not the function body if that makes sense.

Without changing run() and without repeating its body, there's no way to achieve this type trait.

Hope this helps

like image 184
Alex Avatar answered Sep 17 '25 01:09

Alex


To ex&and a bit on your question to Alex's answer above.
This limitation can be overcome in c++ 20, with concepts. Which means it is definitely possible in c++14, using option1, AND using std::enable_if<> in the declaratoion of run() to further restrict access to run(). It would end up looking a bit messy in c++14, but it can be made to work.

Example:

#include <iostream>
#include <type_traits>

// these will help to reduce clutter a bit
template <typename... Ts>
struct make_void {
    typedef void type;
};
template <typename... Ts>
using void_t = typename make_void<Ts...>::type;

// inner_type compatibility type_traits
template <typename T, typename = void>
struct can_add : std::false_type {};

template <typename T>
struct can_add<T,
               std::enable_if_t<std::is_same<
                   T, decltype(std::declval<T>() + std::declval<T>())>::value>>
    : std::true_type {};

// outer type compatibility type_traits
template <typename T, typename = void>
struct has_typedef_t : std::false_type {};

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

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

template <typename T>
struct has_run<T, void_t<decltype(std::declval<T>().run(std::declval<T>()))>>
    : std::true_type {};

template <typename T>
struct is_run_compatible
    : std::integral_constant<bool,
                             has_typedef_t<T>::value && has_run<T>::value> {};

// types under test
struct bogus {};

template <typename Type>
struct not_compatible {
    using T = Type;
};

template <typename Type>
struct compatible {
    using T = Type;

    // here is the correct place to chack for compatibility with Type.
    template <typename R = std::enable_if<can_add<Type>::value, compatible>>
    typename R::type run(const compatible& a) {
        return compatible{val_ + a.val_};
    }

    Type val_;
};

int main() {
    compatible<int> a;
    compatible<int> b;
    auto c = a.run(b);

    std::cout << "can_add<int>: " << can_add<int>::value << std::endl;
    std::cout << "can_add<float>: " << can_add<float>::value << std::endl;
    std::cout << "can_add<bogus>: " << can_add<bogus>::value << std::endl;

    std::cout << "has_typedef_t<int>" << has_typedef_t<int>::value << std::endl;
    std::cout << "has_typedef_t<not_compatible<int>>"
              << has_typedef_t<not_compatible<int>>::value << std::endl;
    std::cout << "has_typedef_t<compatible<bogus>>"
              << has_typedef_t<compatible<bogus>>::value << std::endl;
    std::cout << "has_typedef_t<compatible<int>>"
              << has_typedef_t<compatible<int>>::value << std::endl;

    std::cout << "has_run<int>" << has_run<int>::value << std::endl;
    std::cout << "has_run<not_compatible<int>>"
              << has_run<not_compatible<int>>::value << std::endl;
    std::cout << "has_run<compatible<bogus>>"
              << has_run<compatible<bogus>>::value << std::endl;
    std::cout << "has_run<compatible<int>>" << has_run<compatible<int>>::value
              << std::endl;

    std::cout << "is_run_compatible<int>" << has_run<int>::value << std::endl;
    std::cout << "is_run_compatible<not_compatible<int>>"
              << is_run_compatible<not_compatible<int>>::value << std::endl;
    std::cout << "is_run_compatible<compatible<bogus>>"
              << is_run_compatible<compatible<bogus>>::value << std::endl;
    std::cout << "is_run_compatible<compatible<int>>"
              << is_run_compatible<compatible<int>>::value << std::endl;
}

Program output:

can_add<int>: 1
can_add<float>: 1
can_add<bogus>: 0
has_typedef_t<int>0
has_typedef_t<not_compatible<int>>1
has_typedef_t<compatible<bogus>>1
has_typedef_t<compatible<int>>1
has_run<int>0
has_run<not_compatible<int>>0
has_run<compatible<bogus>>0
has_run<compatible<int>>1
is_run_compatible<int>0
is_run_compatible<not_compatible<int>>0
is_run_compatible<compatible<bogus>>0
is_run_compatible<compatible<int>>1

Alternatively, you could check for functionality at the class level:

template <typename Type, typename = std::enable_if_t<can_add<Type>::value>>
struct compatible {
    using T = Type;

    // here is the correct place to chack for compatibility with Type.
    compatible run(const compatible& a) { return compatible{val_ + a.val_}; }

    Type val_;
};

You can play with this code here: https://godbolt.org/z/vnWd6vcnc

like image 29
Michaël Roy Avatar answered Sep 17 '25 01:09

Michaël Roy