How to use decltype(T::member) for a member that might not exist in T

175 Views Asked by At

I am looking for a way to define a variable with a type depending on the type of the member of a type my class is templated on, with the additional caveat that the member variable might not exist. As I only access the variable in constexpr blocks if the member does exist, I don't care about declaring it or what type it is otherwise. However, I learned that typename conditional<hasMember<T>, decltype(T::member), int> does not work, as the not-used branch still needs to compile.

Here is an example of what I was hoping to get to work:

#include <iostream>
using namespace std;

struct X{};
struct Y{string member;};

template<class T>
concept hasMember = requires (T t) {t.member;};

template<class T>
struct A{
    void hi(T a) {
            // stuff
           typename conditional<hasMember<T>, decltype(T::member), int /*member is unused if hasMember<T> is false, so I don't care what type it is or if it exists*/>::type member;
           
           if constexpr (hasMember<T>){
               member = a.member;
           }
            // stuff
           if constexpr (hasMember<T>){
               std::cout << member<< std::endl;
           }
    };
};

int main() {
    X x;
    Y y{"hi"};
    
    // Does not compile 
    // A<X> ax;
    // ax.hi(x);
    
    // Compiles and runs fine
    A<Y> ay;
    ay.hi(y);

    return 0;
}
4

There are 4 best solutions below

2
Ted Lyngmo On BEST ANSWER

You could create an old-school type trait to get the type instead:

template<class T, class O>
struct member_or {
    static O test(...);

    template<class U = T>
    static auto test(int) -> decltype(U::member);

    using type = decltype(test(0));
};

template<class T, class O>
using member_or_t = member_or<T, O>::type;

Your A implementation could then be:

template <class T>
struct A {
    void hi(T a) {
        // stuff
        member_or_t<T, int> member;    // <- now no problem

        if constexpr (hasMember<T>) {
            member = a.member;
        }
        // stuff
        if constexpr (hasMember<T>) {
            std::cout << member << std::endl;
        }
    };
};
0
Jan Schultke On

@TedLyngmo's solution is solid, and there are more alternatives. You can simply rely on the fact that the compiler lazily instantiates all templates already, and define a class template:

template <typename T>
struct member_type {
    using type = decltype(T::member);
};

As long as you don't access ::type, no instantiation of member_type takes place. This can be exploited in std::conditional_t:

using M = std::conditional_t<hasMember<T>, member_type<T>, std::type_identity<int>>::type;
M member;

Instead of conditionally selecting decltype(T::member) and int, you select a class template for which ::type contains those types. Then, access ::type only for the chosen class template, where std::type_identity is used as a fallback.

0
康桓瑋 On

You can just initialize memebr local variables using lambda like

void hi(T a) {
       auto member = [&] {
         if constexpr (hasMember<T>)
           return a.member;
         else
           return 0;
       }();

        // stuff
       if constexpr (hasMember<T>){
           std::cout << member<< std::endl;
       }
};
0
Jarod42 On

As I only access the variable in constexpr blocks if the member does exist, I don't care about declaring it or what type it is otherwise.

So why use it in non-constexpr block?

You might simply rewrite your function like that:

template<class T>
struct A{
    void hi(T a) {
        // ...
        auto do_stuff = [](){ /*stuff*/ };
        if constexpr (hasMember<T>){
            using MemberType = decltype(T::member);
            const MemberType member = a.member;
            do_stuff();
            std::cout << member << std::endl;
        } else {
            do_stuff();
        }
        // ...
    };
};

Demo