头图

std::is_detected and Detection Idoms

This article locks in the scope of C++17 to talk about implementation.

About std::is_detected

To be precise, it refers to std::experimental::is_detected , std::experimental::detected_t , std::experimental::detected_or . Because they have not been included in the official library, in current compilers, they usually require at least the C++17 specification and include a special header file <experimental/type_traits> . Reference here: cppref .

But compiler support is also uneven. Therefore, we generally use a custom version, which also requires the C++17 specification (if you need a lower-standard adaptation version, please search for it yourself), but the performance ability is more reliable and predictable:

#if !defined(__TRAITS_VOIT_T_DEFINED)
#define __TRAITS_VOIT_T_DEFINED
// ------------------------- void_t
namespace cmdr::traits {
#if (__cplusplus > 201402L)
    using std::void_t; // C++17 or later
#else
    // template<class...>
    // using void_t = void;

    template<typename... T>
    struct make_void { using type = void; };
    template<typename... T>
    using void_t = typename make_void<T...>::type;
#endif
} // namespace cmdr::traits
#endif // __TRAITS_VOIT_T_DEFINED


#if !defined(__TRAITS_IS_DETECTED_DEFINED)
#define __TRAITS_IS_DETECTED_DEFINED
// ------------------------- is_detected
namespace cmdr::traits {
    template<class, template<class> class, class = void_t<>>
    struct detect : std::false_type {};

    template<class T, template<class> class Op>
    struct detect<T, Op, void_t<Op<T>>> : std::true_type {};

    template<class T, class Void, template<class...> class Op, class... Args>
    struct detector {
        using value_t = std::false_type;
        using type = T;
    };

    template<class T, template<class...> class Op, class... Args>
    struct detector<T, void_t<Op<Args...>>, Op, Args...> {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

    struct nonesuch final {
        nonesuch() = delete;
        ~nonesuch() = delete;
        nonesuch(const nonesuch &) = delete;
        void operator=(const nonesuch &) = delete;
    };

    template<class T, template<class...> class Op, class... Args>
    using detected_or = detector<T, void, Op, Args...>;

    template<class T, template<class...> class Op, class... Args>
    using detected_or_t = typename detected_or<T, Op, Args...>::type;

    template<template<class...> class Op, class... Args>
    using detected = detected_or<nonesuch, Op, Args...>;

    template<template<class...> class Op, class... Args>
    using detected_t = typename detected<Op, Args...>::type;

    /**
     * @brief another std::is_detected
     * @details For example:
     * @code{c++}
     * template&lt;typename T>
     * using copy_assign_op = decltype(std::declval&lt;T &>() = std::declval&lt;const T &>());
     * 
     * template&lt;typename T>
     * using is_copy_assignable = is_detected&lt;copy_assign_op, T>;
     * 
     * template&lt;typename T>
     * constexpr bool is_copy_assignable_v = is_copy_assignable&lt;T>::value;
     * @endcode
     */
    template<template<class...> class Op, class... Args>
    using is_detected = typename detected<Op, Args...>::value_t;

    template<template<class...> class Op, class... Args>
    constexpr bool is_detected_v = is_detected<Op, Args...>::value;

    template<class T, template<class...> class Op, class... Args>
    using is_detected_exact = std::is_same<T, detected_t<Op, Args...>>;

    template<class To, template<class...> class Op, class... Args>
    using is_detected_convertible = std::is_convertible<detected_t<Op, Args...>, To>;

} // namespace cmdr::traits
#endif // __TRAITS_IS_DETECTED_DEFINED

Of course, std::void_t is also involved, and it is also C++17 that entered the standard library. But you can declare it yourself, just like the VOID_T part above.

But we gave up on the low version compatibility part of is_detected, which is too long, and I don't want to add multiple unit testing burdens.

It is used like this:

#include <type_traits>
#include <string>

template<typename T>
using copy_assign_op = decltype(std::declval<T &>() = std::declval<const T &>());

template<typename T>
using is_copy_assignable = is_detected<copy_assign_op, T>;

template<typename T>
constexpr bool is_copy_assignable_v = is_copy_assignable<T>::value;

struct foo {};
struct bar {
  bar &operator=(const bar &) = delete;
};

int main() {
  static_assert(is_copy_assignable_v<foo>, "foo is copy assignable");
  static_assert(!is_copy_assignable_v<bar>, "bar is not copy assignable");
  return 0;
}

You can see that this is a typical detection idioms idiom, which can test out what characteristics a type has at compile time. For example, is_chrono_duration, is_iterator, is_integer, etc. There are a large number of predefined traits for detection in the standard library.

But in real life we usually need to define our own. For example, there is a group undo-cxx

namespace undo_cxx {

  template<typename State,
  typename Context,
  typename BaseCmdT,
  template<class S, class B> typename RefCmdT,
  typename Cmd>
    class undoable_cmd_system_t {
      public:
      ~undoable_cmd_system_t() = default;

      using StateT = State;
      using ContextT = Context;
      using CmdT = Cmd;
      using CmdSP = std::shared_ptr<CmdT>;
      using Memento = typename CmdT::Memento;
      using MementoPtr = typename std::unique_ptr<Memento>;
      using Container = std::list<MementoPtr>;
      using Iterator = typename Container::iterator;

      using size_type = typename Container::size_type;

      template<typename T, typename = void>
      struct has_save_state : std::false_type {};
      template<typename T>
      struct has_save_state<T, decltype(void(std::declval<T &>().save_state()))> : std::true_type {};

      template<typename T, typename = void>
      struct has_undo : std::false_type {};
      template<typename T>
      struct has_undo<T, decltype(void(std::declval<T &>().undo()))> : std::true_type {};

      template<typename T, typename = void>
      struct has_redo : std::false_type {};
      template<typename T>
      struct has_redo<T, decltype(void(std::declval<T &>().redo()))> : std::true_type {};

      template<typename T, typename = void>
      struct has_can_be_memento : std::false_type {};
      template<typename T>
      struct has_can_be_memento<T, decltype(void(std::declval<T &>().can_be_memento()))> : std::true_type {};

      public:
      
      // ...
      
      void undo(CmdSP &undo_cmd) {
        if constexpr (has_undo<CmdT>::value) {
          // needs void undo_cmd::undo(sender, ctx, delta)
          undo_cmd->undo(undo_cmd, _ctx, 1);
          return;
        }

        if (undo_one()) {
          // undo ok
        }
      }
      
      // ...
    };
}

You may notice that is_detected is not used at all in this example.

Indeed, Detection Idioms contains a series of techniques. It is not necessary to use which tool template. The focus is on the target. They are all designed to test a certain type of feature at compile time, so as to make targeted specialization and bias. Specialized, or used to complete other tasks.

So this is a huge topic.

Detection Idioms

In the proposal [WG21 N4436-Proposing Standard Library Support for the C++ Detection Idiom [pdf]]( http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4436.pdf) , The detection idiom is called Detection Idiom. This proposal was added in C++20, partly because it can be a complementary solution, and on the other hand because Concepts has been mentioned for more than ten years but there is no conclusion. Of course, we finally learned that concepts were finally added after C++20 was finalized last year.

However, it is foreseeable that until 2023, the possibility of using concepts in engineering will still be basically zero. In fact, if C++17 can become the mainstream of engineering applications in 2023, it will be Amitabha. Most of the projects are still C++11, and those legacy projects don’t even use C++11.

As a representative of a proposal, Detection Idiom is a proper noun. But testing idioms has long existed. In other words, traits originally did this. The detection idioms in this article will include type testing and constraints, as well as detection methods such as function signature detection.

After C++11, with the help of SFINAE, we have several options to complete type constraints and selections: specialization, adding specialization of enable_if test, with the help of is_detected binding.

Ordinary specialization

The specialization ability of template parameters is sufficient for general constraints:

template<typename T>
bool max(T l, T r) { return l > r ? l : r; }

bool max(bool l, bool r) { return false; }

bool max(float l, float r) {
  return (l+0.000005f > r) ? l : (r+0.000005f>l) ? r : IS_NAN;
}

The above example is of no practical use, just to show the direct use effect of the specialization.

For simple types, this specialization is sufficient. However, when it comes to complex types, especially complex types, its ability is relatively short.

So in this case we need to use enable_if to constrain.

std::enable_if method

Possible implementation methods of std::enable_if

The implementation method of std::enable_if can be relatively simple:

template <bool, typename T=void>
struct enable_if {};

template <typename T>
struct enable_if<true, T> {
  using type = T;
};
Constraint return type

For function return types, the usage is slightly different:

#include <iostream>
#include <type_traits>

class foo;
class bar;

template<class T>
struct is_bar {
    template<class Q = T>
    typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check() { return true; }

    template<class Q = T>
    typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check() { return false; }
};

int main() {
    is_bar<foo> foo_is_bar;
    is_bar<bar> bar_is_bar;
    if (!foo_is_bar.check() && bar_is_bar.check())
        std::cout << "It works!" << std::endl;

    return 0;
}

This is a use case of testing type and returning bool, and it is also a typical use case of how to write traits.

But in order to really illustrate the templating of function return types, the following example is still needed:

#include <iostream>
#include <type_traits>

namespace AAA {
    template<class T>
    class Y {
    public:
        template<typename Q = T>
        typename std::enable_if<std::is_same<Q, double>::value || std::is_same<Q, float>::value, Q>::type foo() {
            return 11;
        }
        template<typename Q = T>
        typename std::enable_if<!std::is_same<Q, double>::value && !std::is_same<Q, float>::value, Q>::type foo() {
            return 7;
        }
    };
} // namespace

int main(){
#define TestQ(typ)  std::cout << "T foo() : " << (AAA::Y<typ>{}).foo() << '\n'

    TestQ(short);
    TestQ(int);
    TestQ(long);
    TestQ(bool);
    TestQ(float);
    TestQ(double);  
}

The output is:

T foo() : 7
T foo() : 7
T foo() : 7
T foo() : 1
T foo() : 11
T foo() : 11

This is a practical use case, which can directly detect double or float types.

Use in traits

Test whether there is a type definition named value in a template:

template <typename T, typename=void>
struct has_typed_value;

template <typename T>
struct has_typed_value<T, typename std::enable_if<T::value>::type> {
    static constexpr bool value = T::value;
};

template<class T>
inline constexpr bool has_typed_value_v = has_typed_value<T>::value;

static_assert(has_typed_value<std::is_same<bool, bool>>::value, "std::is_same<bool, bool>::value is valid");
static_assert(has_typed_value_v<std::is_same<bool, bool>>, "std::is_same<bool, bool>::value is valid");

Similarly:

template <typename T> struct has_typed_type;
template <typename T>
struct has_typed_type<T, typename std::enable_if<T::value>::type> {
    static constexpr bool value = T::type;
};

template<class T>
inline constexpr bool has_typed_type_v = has_typed_type<T>::value;

Use conjuction

After C++17, you can directly use std::conjuction , which can be used to combine a set of detectors.

Here is only a sample fragment:

template <class T>
  using is_regular = std::conjunction<std::is_default_constructible<T>,
    std::is_copy_constructible<T>,
    supports_equality<T,T>,
    supports_inequality<T,T>, //assume impl
    supports_less_than<T,T>>; //ditto

more

Check the existence of member functions

declval way

This use case is relatively independent, everything is done by yourself, void_t is defined by itself, and traits named supports_foo are defined by itself, the purpose is to detect whether the type T has the T::get_foo() function signature. Finally, the special purpose of calculate_foo_factor() is obvious, and no explanation is needed.

template <class... Ts>
using void_t = void;

template <class T, class=void>
struct supports_foo : std::false_type{};

template <class T>
struct supports_foo<T, void_t<decltype(std::declval<T>().get_foo())>>
: std::true_type{};

template <class T, 
          std::enable_if_t<supports_foo<T>::value>* = nullptr>
auto calculate_foo_factor (const T& t) {
  return t.get_foo();
}

template <class T, 
          std::enable_if_t<!supports_foo<T>::value>* = nullptr>
int calculate_foo_factor (const T& t) {
  // insert generic calculation here
  return 42;
}

It uses declval's forged instance technology, for this you can refer to our std::declval and decltype article.

Is_detected method

Use the is_detected method instead:

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

template<typename T>
constexpr bool has_to_string = is_detected_v<to_string_t, T>;

struct AA {
  std::string to_string() const { return ""; }
};

struct BB{};

static_assert(has_to_string<AA>, "");
static_assert(!has_to_string<BB>, "");

There is nothing to say about this, you can use std::experimental::is_detected, or use the is_detected tool defined in the previous article.

As a supplement

When there is no std::enable_if, the struct char[] method is needed. This technique is called Member Detector , which is a classic idiom before C++11:

template<typename T>
class DetectX
{
    struct Fallback { int X; }; // add member name "X"
    struct Derived : T, Fallback { };

    template<typename U, U> struct Check;

    typedef char ArrayOfOne[1];  // typedef for an array of size one.
    typedef char ArrayOfTwo[2];  // typedef for an array of size two.

    template<typename U> 
    static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
    
    template<typename U> 
    static ArrayOfTwo & func(...);

  public:
    typedef DetectX type;
    enum { value = sizeof(func<Derived>(0)) == 2 };
};

And there is a matching macro definition GENERATE_HAS_MEMBER(member) , so only need in the application code:

GENERATE_HAS_MEMBER(att)  // Creates 'has_member_att'.
GENERATE_HAS_MEMBER(func) // Creates 'has_member_func'.

std::cout << std::boolalpha
  << "\n" "'att' in 'C' : "
  << has_member_att<C>::value // <type_traits>-like interface.
    << "\n" "'func' in 'C' : "
    << has_member_func<C>() // Implicitly convertible to 'bool'.
    << "\n";

It's crazy too.

Further extension

The technique provided in the previous section only detects the function name itself in the function signature. Sometimes, we may want to check and constrain the formal parameter list or return type. is it possible?

There is indeed.

Extract function return type

return_type_of_t<Callable> can extract the return type of the function:

namespace AA1 {
  template<typename Callable>
  using return_type_of_t =
  typename decltype(std::function{std::declval<Callable>()})::result_type;

  int foo(int a, int b, int c, int d) {
    return 1;
  }
  auto bar = [](){ return 1; };
  struct baz_ { 
    double operator()(){ return 0; } 
  } baz;

  void test_aa1() {
    using ReturnTypeOfFoo = return_type_of_t<decltype(foo)>;
    using ReturnTypeOfBar = return_type_of_t<decltype(bar)>;
    using ReturnTypeOfBaz = return_type_of_t<decltype(baz)>;

    // ...
  }
}

On this basis, you can use enable_if or is_detected. The specific test is a little bit...

Check function parameter list

As for the detection of formal parameter tables, it is a bit cumbersome, and this topic is also very big and has many directions, so I will give you one direction for your reference for the time being, and the other directions are invariable.

bar_t can list the type table in the form of variadic parameters, and is used to check whether the type T has a function bar() and also has a corresponding formal parameter table:

template<class T, typename... Arguments>
using bar_t = std::conditional_t<
        true,
        decltype(std::declval<T>().bar(std::declval<Arguments>()...)),
        std::integral_constant<
                decltype(std::declval<T>().bar(std::declval<Arguments>()...)) (T::*)(Arguments...),
                &T::bar>>;

struct foo1 {
    int const &bar(int &&) {
        static int vv_{0};
        return vv_;
    }
};

static_assert(dp::traits::is_detected_v<bar_t, foo1, int &&>, "not detected");

For the time being, I have no idea to generalize it, so you need to copy and modify it in specific situations.

If anyone has an improved version, please let me know.

Detect the existence of begin() and call it

I have already introduced the existence of member functions in the previous article. Here is a practical snippet: check the existence of a member function, call it if it exists, or call our alternate implementation:

// test for `any begin() const`
template <typename T>
using begin_op = decltype(std::declval<T const&>().begin());

struct A_Container {
  template <typename T>
  void invoke_begin(T const& t){
    if constexpr(std::experimental::is_detected_v<begin_op, T>){
      t.begin();
    }else{
      my_begin(); // begin() not exists!!
    }
  }
  iterator my_begin() { ... }
};

Why not use SFINAE technology directly?

Because SFINAE technology allows us to make a specialized version of invoke_begin, but this may hinder our further scalability. SFINAE can only work when the types do not match, but we can use the begin_op method: for the return type, for the formal parameter list, for const or not, and so on.

Check emplace(Args &&...)

This will use our previous bar_t technique:

template<class T, typename... Arguments>
  using emplace_variadic_t = std::conditional_t<
  true,
decltype(std::declval<T>().emplace(std::declval<Arguments>()...)),
std::integral_constant<
  decltype(std::declval<T>().emplace(std::declval<Arguments>()...)) (T::*)(Arguments...),
&T::emplace>>;

/**
 * @brief test member function `emplace()` with variadic params
 * @tparam T 
 * @tparam Arguments 
 * @details For example:
 * @code{c++}
 * using C = std::list&lt;int>;
 * static_assert(has_emplace_variadic_v&lt;C, C::const_iterator, int &&>);
 * @endcode
 */
template<class T, typename... Arguments>
  constexpr bool has_emplace_variadic_v = is_detected_v<emplace_variadic_t, T, Arguments...>;

namespace detail {
  using C = std::list<int>;
  static_assert(has_emplace_variadic_v<C, C::const_iterator, int &&>);
} // namespace detail

More...

There is a set of detection traits in cmdr-cxx , which are used to detect the signature of standard library container functions. In undo-cxx in undoable_cmd_system_t<State> , we used the technique of detecting the existence of the function name and calling it.

About _t and _v

In the standard library and standardization implementation, the _t and _v implied the direct provision of members of ::type or ::value

But this does not affect my (we) used to using _t represent a template class.

When a template class is used as a base class and used as a tool class, I (we) like to add the _t suffix to it.

Refs

postscript

Detecting idioms is really too big, this article is just a guide to provide basic tools. I should continue to introduce my experience on this issue in the future.

:end:


hedzr
95 声望19 粉丝