Check if an item inside a tuple contains a specific method

Viewed 122

I have a tuple and I'm trying to call initialize() for each object inside the tuple. Now, some objects might not contain that function. I know I can make that work by using C++20 requires feature to check if a class contains a given function. Unfortunately, I'm using MSVC and it doesn't support that yet.

Is it possible to write that differently ? I can use C++20 but it should compile on MSVC

template <class... Args>
struct CustomTuple : std::tuple<Args...> {
  // Calls "initialize() for each item inside the tuple if that function exists"
  template <std::size_t I = 0>
  auto initialize() {
       std::decay_t<decltype(std::get<I>(*this))> item;
       constexpr bool has_initialize = requires() { // doesnt't work on MSVC
           item.initialize(); 
       };
       if constexpr (has_initialize) {
           std::get<I>(*this).initialize();
       }
       if constexpr (I + 1 != sizeof...(Args)) {
           initialize<I + 1>();
    }
  }
};
4 Answers

If you don't have access to requires yet, then you will have to make use of SFINAE and write a type-trait for detecting whether initialize() is a callable member function.

This can be done with std::void_t, albeit a little awkwardly:

#include <type_traits> // std::void_t
#include <utility>     // std::declval

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

template <typename T>
struct has_initialize_impl<T, 
  std::void_t<decltype(std::declval<T&>().initialize())>
> : std::true_type {};

template <typename T>
inline constexpr auto has_initialize = has_initialize_impl<T>::value;

The above trait checks if any mutable reference T& can call a function named initialize(). If the expression is well-formed, then has_initialized evaluates to true; otherwise, it evaluates to false.

This can then be used with the rest of your code with little changes:

template <class... Args>
struct CustomTuple : std::tuple<Args...> {
  // Calls "initialize() for each item inside the tuple if that function exists"
  template <std::size_t I = 0>
  auto initialize() {
       // Note: no need to construct a temporary/unused 'item' type,
       // we can just use 'tuple_element' here to get the type
       using element_type = typename std::tuple_element<I, std::tuple<Args...>>::type;
       
       if constexpr (has_initialize<element_type >) {
           std::get<I>(*this).initialize();
       }
       if constexpr (I + 1 != sizeof...(Args)) {
           initialize<I + 1>();
    }
  }
};

Live Example


If you think you might have a lot more cases where you need to detect functionality, but still won't have C++20 requires support, then it might be worthwhile looking into adding the "detected"-idiom + boilerplate into your code, which can be found on cppreference.

This behaves identically to what std::void_t does, but is more convenient if needed often. This is still more obtuse than c++20's requires, but you don't have many options if your compiler does not support this.

With is_detected_v, all you will need is to define template type aliases of any expressions that you need to validate, and simply pass it in to the trait.

For example, the above solution can be solved succinctly as:

template <typename T>
using detect_initialize = decltype(std::declval<T&>().initialize());

...

if constexpr (is_detected_v<detect_initialize, decltype(item)>) {
    ...
}

Live Example

Another way with SFINAE (and no recursion)

struct low_priority {};
struct high_priority : low_priority {};

template <typename T>
auto initialize_impl(T& t, high_priority) -> decltype(t.initialize())
{ t.initialize(); }

template <typename T>
void initialize_impl(T&, low_priority) { /*Empty*/ }



template <class... Args>
struct CustomTuple : std::tuple<Args...> {
  // Calls "initialize() for each item inside the tuple if that function exists"
  void initialize() {
       std::apply([](auto&... args)
           {
               (initialize_impl(args, high_priority{}), ...);
           },
           *this);
  }
};

Note: priority could be replaced by appropriate concept in C++20.

You can define your own concept instead.

template<typename T>
concept has_initialize = requires(T a) {
    a.initialize();
};

template <class... Args>
struct CustomTuple : std::tuple<Args...> {
  // Calls "initialize() for each item inside the tuple if that function exists"
  template <std::size_t I = 0>
  auto initialize() {
       std::decay_t<decltype(std::get<I>(*this))> item;
       if constexpr (has_initialize<decltype(item)>) {
           std::get<I>(*this).initialize();
       }
       if constexpr (I + 1 != sizeof...(Args)) {
           initialize<I + 1>();
    }
  }
};

https://godbolt.org/z/rvhPPsqcz

Using C++17 features and spiting problem into smaller functions:

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

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

template<typename T>
void tryInitialize_helper(T&& x, std::true_type)
{
    std::forward<T>(x).initialize();
}

template<typename T>
void tryInitialize_helper(T&& x, std::false_type)
{}

template<typename T>
void initialize_if_possible(T&& x)
{
    tryInitialize_helper(std::forward<T>(x), has_inistialize_t<T>{});
}

template<typename ...Ts>
void initilize_each_if_possible(std::tuple<Ts...>& t)
{
    std::apply([](auto&... args){(initialize_if_possible(args), ...);}, t);
}

https://godbolt.org/z/vcEnjY7vE

Related