check if member exists using enable_if
Asked Answered
S

8

34

Here's what I'm trying to do:

template <typename T> struct Model
{
    vector<T> vertices ;

    #if T has a .normal member
    void transform( Matrix m )
    {
        each vertex in vertices
        {
          vertex.pos = m * vertex.pos ;
          vertex.normal = m * vertex.normal ;
        }
    }
    #endif

    #if T has NO .normal member
    void transform( Matrix m )
    {
        each vertex in vertices
        {
          vertex.pos = m * vertex.pos ;
        }
    }
    #endif
} ;

I've seen examples of using enable_if, but I cannot understand how to apply enable_if to this problem, or if it even can be applied.

Simulcast answered 9/12, 2012 at 11:12 Comment(3)
enable_if is not used to check if a member exists, rather it is used to remove overloads.Alkmaar
Can't I use it to do something like (suggestion in edit above)?Simulcast
No, you're wanting a static if which doesn't exist yet. What you want is completely possible, it just won't use syntax like that.Alkmaar
M
35

This has become way easier with C++11.

template <typename T> struct Model
{
    vector<T> vertices;

    void transform( Matrix m )
    {
        for(auto &&vertex : vertices)
        {
          vertex.pos = m * vertex.pos;
          modifyNormal(vertex, m, special_());
        }
    }

private:
    
    struct general_ {};
    struct special_ : general_ {};
    template<typename> struct int_ { typedef int type; };

    template<typename Lhs, typename Rhs,
             typename int_<decltype(Lhs::normal)>::type = 0>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, special_) {
       lhs.normal = rhs * lhs.normal;
    }

    template<typename Lhs, typename Rhs>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, general_) {
       // do nothing
    }
};

Things to note:

  • You can name non-static data members in decltype and sizeof without needing an object.
  • You can apply extended SFINAE. Basically any expression can be checked and if it is not valid when the arguments are substituted, the template is ignored.
Moats answered 9/12, 2012 at 12:51 Comment(5)
That is definitely much better SFINAE than before, where you had to create a member detector for each member type you want to detect. But why do people always jump to SFINAE, I prefer to use type traits to do this type of conditional compilation now.Simulcast
I guess that depends on whether you have more types to check or more members to check?Lorimer
Type traits only helps if you know the types ahead of time. In your link, you have to know that VertexN is the special class. So they may work in OP's example, but SFINAE works even if you don't know what the other types are going to be, which IMO is much better encapsulation.Camire
I liked the use of general_ and special_, I found it clearer than using int and long.Silicate
Note that above works only for fields; checking method existence (like decltype(&Lhs::myFunc)) results to error: "ambiguous call to overloaded function".Nova
D
12

I know this question already has some answers but I think my solution to this problem is a bit different and could help someone.

The following example checks whether passed type contains c_str() function member:

template <typename, typename = void>
struct has_c_str : false_type {};

template <typename T>
struct has_c_str<T, void_t<decltype(&T::c_str)>> : std::is_same<char const*, decltype(declval<T>().c_str())>
{};

template <typename StringType,
          typename std::enable_if<has_c_str<StringType>::value, StringType>::type* = nullptr>
bool setByString(StringType const& value) {
    // use value.c_str()
}

In case there is a need to perform checks whether passed type contains specific data member, following can be used:

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

template <typename T>
struct has_field<T, std::void_t<decltype(T::field)>> : std::is_convertible<decltype(T::field), long>
{};

template <typename T,
          typename std::enable_if<has_field<T>::value, T>::type* = nullptr>
void fun(T const& value) {
    // use value.field ...
}

UPDATE C++20

C++20 introduced constraints and concepts, core language features in this C++ version.

If we want to check whether template parameter contains c_str member function, then, the following will do the work:

template<typename T>
concept HasCStr = requires(T t) { t.c_str(); };

template <HasCStr StringType> 
void setByString(StringType const& value) {
    // use value.c_str()
}

Furthermore, if we want to check if the data member, which is convertible to long, exists, following can be used:

template<typename T>
concept HasField = requires(T t) {
    { t.field } -> std::convertible_to<long>;
};

template <HasField T> 
void fun(T const& value) {
    // use value.field
}

By using C++20, we get much shorter and much more readable code that clearly expresses it's functionality.

Decorator answered 10/11, 2019 at 14:4 Comment(2)
The C++20 version is by far the best and most readable solution. I have (ab)used this to effectively add a default-implementation of a member function post-hoc to an existing library of types, without modifying them to inherit a default implementation from a base class: template <class T> auto call_member(T t) { if constexpr(HasMember<T>) { return t.member(); } else { return default_action(); } }. (A concept requirement is just a constexpr boolean expression, so it can be used in if constexpr like that just fine)Blesbok
The non-C++20 part produces for C++14: error: no template named 'void_t' in namespace 'std'; did you mean '__void_t' (while accepted answer seems to work even in C++11).Nova
K
9

You need a meta function to detect your member so that you can use enable_if. The idiom to do this is called Member Detector. It's a bit tricky, but it can be done!

Kelson answered 9/12, 2012 at 11:35 Comment(0)
S
4

While C++20's requires keyword has been mentioned, the code that's provided is still too complex for your needs, requiring the creation of a separate function for each case. Here's much simpler code for your use case, where a single function implementation suffices:

template <typename T> struct Model
{
   vector<T> vertices ;

   void transform( Matrix m )
   {
      each vertex in vertices
      {
         vertex.pos = m * vertex.pos ;
         if constexpr (requires { &vertex.normal; })
            vertex.normal = m * vertex.normal ;
      }
   }
} ;

Notes:

  • All the trick is on the if constexpr line. I've left your pseudo code as is, but removed the redundancy and added the if constexpr line.

  • The requires expression I've added simply attempts to access the address of the normal member, and evaluates to false if the expression is invalid. You could really use any expression that will succeed if normal is defined and fail if it's not.

  • For classes that do have normal, make sure the member is accessible from this code (e.g. it's either public or an appropriate friendship is specified). Otherwise the code would ignore the normal member as if it didn't exist at all.

  • See the "Simple requirements" section at https://en.cppreference.com/w/cpp/language/constraints for more information.

Silber answered 7/6, 2022 at 21:27 Comment(0)
A
3

This isn't an answer to your exact case, but it is an alternative answer to the question title and problem in general.

#include <iostream>
#include <vector>

struct Foo {
    size_t length() { return 5; }
};

struct Bar {
    void length();
};

template <typename R, bool result = std::is_same<decltype(((R*)nullptr)->length()), size_t>::value>
constexpr bool hasLengthHelper(int) { 
    return result;
}

template <typename R>
constexpr bool hasLengthHelper(...) { return false; }

template <typename R>
constexpr bool hasLength() {
    return hasLengthHelper<R>(0);
}

// function is only valid if `.length()` is present, with return type `size_t`
template <typename R>
typename std::enable_if<hasLength<R>(), size_t>::type lengthOf (R r) {
  return r.length();
}

int main() {
    std::cout << 
      hasLength<Foo>() << "; " <<
      hasLength<std::vector<int>>() << "; " <<
      hasLength<Bar>() << ";" <<
      lengthOf(Foo()) <<
      std::endl;
    // 1; 0; 0; 5

    return 0;
}

Relevant https://ideone.com/utZqjk.

Credits to dyreshark on the freenode IRC #c++.

Anetta answered 15/3, 2017 at 5:49 Comment(0)
Z
3
template<
typename HTYPE, 
typename = std::enable_if_t<std::is_same<decltype(HTYPE::var1), decltype(HTYPE::var1)>::value>
>
static void close_release
(HTYPE* ptr) {
    ptr->var1;
}

Using enable_if and decltype to let compiler to check variable, hope to help.

Zinn answered 21/3, 2020 at 10:2 Comment(1)
std::is_same<decltype(HTYPE::var1), decltype(HTYPE::var1)>::value could be more simply written as std::is_pointer<decltype(&HTYPE::var1)>::value. Plus, adding the & avoids a compiler error when checking for the presence of a function.Drumhead
C
1

I know that it's little late, however...

typedef int Matrix;

struct NormalVertex {
    int pos;
    int normal;
};

struct Vertex {
    int pos;
};

template <typename T> struct Model
{
    typedef int No;
    typedef char Yes;

    template<typename U> static decltype (declval<U>().normal, Yes()) has_normal(U a);
    static No has_normal(...);

    vector<T> vertices ;

    template <typename U = T>
    typename enable_if<sizeof(has_normal(declval<U>())) == sizeof(Yes), void>::type
    transform( Matrix m )
    {
        std::cout << "has .normal" << std::endl;
        for (auto vertex : vertices)
        {
          vertex.pos = m * vertex.pos ;
          vertex.normal = m * vertex.normal ;
        }
    }

    template <typename U = T>
    typename enable_if<sizeof(has_normal(declval<U>())) == sizeof(No), void>::type
    transform( Matrix m )
    {
        std::cout << "has no .normal" << std::endl;
        for (auto vertex : vertices)
        {
          vertex.pos = m * vertex.pos ;
        }
    }
} ;

int main()
{
    Matrix matrix;
    Model <NormalVertex> normal_model;

    Vertex simple_vertex;
    Model <Vertex> simple_model;

    simple_model.transform(matrix);
    normal_model.transform(matrix);

    return 0;
}
Char answered 5/11, 2019 at 6:14 Comment(0)
S
1

I had a similar issue and my solution was to use boost's BOOST_TTI_HAS_MEMBER_DATA macro.

#include <boost/tti/has_member_data.hpp>

BOOST_TTI_HAS_MEMBER_DATA(normal)

template <typename T> struct Model
{
    vector<T> vertices;
    static constexpr bool hasNormal = has_member_data_normal<T, double>::value;

    template<bool B = hasNormal, std::enable_if_t<B, int> = 0>
    void transform( Matrix m )
    {
        for(auto&& vertex : vertices)
        {
          vertex.pos = m * vertex.pos ;
          vertex.normal = m * vertex.normal ;
        }
    }
    
    template<bool B = hasNormal, std::enable_if_t<!B, int> = 0>
    void transform( Matrix m )
    {
        for(auto&& vertex : vertices)
        {
          vertex.pos = m * vertex.pos ;
        }
    }
};

If you don't want to be dependent on boost, then you can use @ltjax's answer to create your own has_member_data_normal struct.

Stickney answered 3/10, 2022 at 18:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.