Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define a concept of a object that is can be structured binding?

I want to define a concept that can detect whether the type T can be structured binding or not:

template <typename T>
concept two_elements_structured_bindable = requires (T t) {
  auto [x, y] = t;
};

but this cannot be compiled. Is there a proper way to define a concept like that?

like image 932
康桓瑋 Avatar asked Oct 16 '22 01:10

康桓瑋


2 Answers

With C++20, you can define a concept that would identify C-style arrays and tuple-like types as structure bindable. But it cannot identify types which are being structure bindable based on public-only fields.

Possible concepts that can be implemented (see full implementation here):

template<typename T, std::size_t N>
concept structure_bindable = 
    (std::is_array_v<T> && (std::extent_v<T> == N)) ||
    ((std::tuple_size_v<T> == N) && has_get_for_size<T, N>::value);

template<typename T, typename... Ts>
concept structure_bindable_with = 
    structure_bindable<T, sizeof...(Ts)>
    && is_get_N<T, Ts...>(std::make_index_sequence<sizeof...(Ts)>{});

template<typename T, size_t N, typename Expected>
concept structure_bindable_with_N = 
    structure_bindable<T, N>
    && is_get_N<T, N-1, Expected>();

A side note: it can be achieved with compiler intrinsic features for example - here for clang (courtesy of Avi Lachmish).


My dear friend @Dvir Yitzchaki pointed out to me that based on the proposed pattern matching syntax by Herb Sutter, you can identify all stucture bindable cases, based on as check inside a concept, not yet in C++20, but already implemented in Circle compiler.

Herb Sutter together with the Circle compiler implementer Sean Baxter, presented the idea of the is and as pattern matching, as part of Herb's talk at CppCon 2021, see here.

Based on their talk, Dvir came to the idea that I later elaborated into this working implementation on Circle compiler:

template <typename T>
concept two_elements_structured_bindable = structured_bindable<T>
    && !single_element_structured_bindable<T>
    && two_elements_structured_bindable_<T>;

Based on this:

template <typename T>
concept structured_bindable = requires (T t) {
    t as [...]; // note: not supported by C++20
};

template <typename T>
struct single_element_structured_bindable_wrapper {
    auto first() {
        auto[a, ...] = std::declval<T>();  // note: not supported by C++20
        return a;
    }
    using first_type = decltype(first());
};

template <typename T>
concept single_element_structured_bindable = structured_bindable<T>
    && requires (T t) {
    {t as [single_element_structured_bindable_wrapper<T>::first_type]};
};

And:

template <typename T>
struct two_elements_structured_bindable_wrapper {
    auto first() {
        auto[a, ...] = std::declval<T>(); // note: not supported by C++20
        return a;
    }
    auto second() {
        auto[a, b, ...] = std::declval<T>(); // note: not supported by C++20
        return b;
    }
    using first_type = decltype(first());
    using second_type = decltype(second());
};

template <typename T>
concept two_elements_structured_bindable_ = requires (T t) {
    {t as [
            two_elements_structured_bindable_wrapper<T>::first_type,
            two_elements_structured_bindable_wrapper<T>::second_type
          ]};
};

Note that it supports checking all type of structure bindings:

static_assert(!two_elements_structured_bindable<std::tuple<int, int, int>>);
static_assert(!two_elements_structured_bindable<std::tuple<int>>);
static_assert(!two_elements_structured_bindable<int>);
static_assert(two_elements_structured_bindable<std::tuple<int, int>>);

static_assert(!two_elements_structured_bindable<std::array<int, 3>>);
static_assert(!two_elements_structured_bindable<std::array<int, 1>>);
static_assert(two_elements_structured_bindable<std::array<int, 2>>);

struct Vec3 { float x, y, z; };    
static_assert(!two_elements_structured_bindable<Vec3>);

struct Vec2 { float x, y; };
static_assert(two_elements_structured_bindable<Vec2>);

After presenting above solution in a CoreCpp meetup, Dvir poured cold water on my solution, with a much shorter one:

struct anything // std::any is not good enough for that
{
    template<typename T>
    anything(T&&) {}
};

template<typename T>
concept twople = requires(T t)
{
    t as [anything, anything];
};

I still keep the long solution above, as it has in my view some value for other implementations.


If you want to constraint on the types you would bound with, you may prefer going with another approach, which again relies on pattern matching syntax, with another proposed concept:

template <typename T, typename... Ts>
concept TupleLike = requires (T t) {
    {t as [Ts...]}; // note: not supported by C++20
};

That can allow a constraint like this:

void foo(TupleLike<double, double, double> auto tup) {
    auto[a, b, c] = tup; // 3 doubles
    // ...
}

Above code is again based on pattern matching syntax, not yet available in C++ (as of C++20), but already implemented in Circle compiler.


Now, in order to support a more generic concept for structured_bindable<Size>, there is a need to use another future C++ feature, which allows sizeof...(T) on a T that is not a variadic pack but rather any structure bindable type. This feature might be part of p1858 or a related proposal. And again, it is already supported in Circle compiler.

This allows this very simple implementation (again, proposed by Dvir):

template <typename T, size_t SIZE>
concept has_size_of = sizeof...(T) == SIZE;

template <typename T>
concept structured_bindable = requires (T t) {
    t as [...];
};

template <typename T, size_t SIZE>
concept structured_bindable_with = structured_bindable<T> && has_size_of<T, SIZE>;

Thus allowing constraining on being bindable with an exact given number, e.g.:

void foo(const structured_bindable_with<2> auto& v) {
    const auto&[a, b] = v;
    std::cout << a << ", " << b << std::endl;
}

In fact, if being able to provide sizeof... becomes a feature that says you are a structure bindable type (including variadic pack by itself!), then above concept can simply become:

template <typename T, size_t SIZE>
concept structured_bindable_with = (sizeof...(T) == SIZE);
like image 62
Amir Kirsh Avatar answered Nov 15 '22 11:11

Amir Kirsh


No.

There are three cases in structured bindings:

  1. Arrays. That's easy enough to detect.

  2. Tuple-like. You can easily check if std::tuple_size<E>::value is valid, and then check if std::tuple_element<I, E>::type is valid as a type for all the right types. But the get case is harder since you have to deal with member vs non-member... but otherwise I think doable.

  3. Types which have all public (yeah yeah, technically accessible) members as direct members of the same class. This is impossible to detect with current technology. magic_get can, I think, handle struct X { int a, b; }; but neither struct Y : X { }; nor struct Z { X& x; }; You would need to have proper reflection to check this case.

As of C++20, you would need some kind of compiler intrinsic to do this.

like image 40
Barry Avatar answered Nov 15 '22 09:11

Barry