Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a concept for structured bindings?

While compiling the following (reduced) code:

#include <tuple>
#include <stdlib.h>

template<size_t N> struct tying;
template<> struct tying<1> {static auto function(auto& o) -> decltype(auto) {auto& [p1]    = o;return std::tie(p1);}};
template<> struct tying<2> {static auto function(auto& o) -> decltype(auto) {auto& [p1,p2] = o;return std::tie(p1,p2);}};

template<typename T, size_t N> concept bool n_components =
requires(T& object) {
    { tying<N>::function(object) };
};

typedef struct
{
    int   a;
    float b;
} test_t;

int main(int argc, char* argv[])
{
    constexpr size_t n = 1;
    constexpr bool   t = n_components<test_t, n>;

    printf("n_components<test_t, %d>: %s\n", n, t ? "yes" : "nope");
    return 0;
}

with gcc specific options -std=c++1z -fconcepts the gcc (version 7.3.0, and x86-64 gcc-trunk at godbolt also) fails with an error:

error: only 1 name provided for structured binding
note: while 'test_t' decomposes into 2 elements

I was very surprised by this, as the error "raises" inside the requires-expression, and to my understanding, this should result in n_components constraint to get evaluated to false, as the requirement is not satisfied.

Note: replacing constexpr size_t n = 1; with other values "fixes" the error, so I presume the n_components constraint is not to blame:

  • replacing with n = 2 the n_components constraint evaluates to "true" as expected.
  • replacing with n = 3 the n_components constraint evaluates to "false" as expected - due to referring to unspecialized tying<3> struct.

It seems there are no other compilers supporting the c++ concepts and structured bindings available yet to compare with.

PS. I was playing around with "poor man's reflection" a.k.a. magic_get and hoped to replace the unreliable is_braces_constructible trait with something more robust...

like image 865
qwe Avatar asked Mar 17 '18 19:03

qwe


2 Answers

Here's a partial solution for clang since 5.0.
It is distilled from a recent reddit post Find the number of structured bindings for any struct

The method is non-standard, using gnu extension statement expressions to effectively SFINAE on structured binding declarations. It is also non-portable as gcc fails to parse structured binding in statement expression in a lambda trailing return type.

template <typename... Ts> struct overloads : Ts... { using Ts::operator()...; };
template <typename... Ts> overloads(Ts...) -> overloads<Ts...>;

template <typename T>
auto num_bindings_impl() noexcept {
    return overloads{
        [](auto&& u, int) -> decltype(({auto&& [x0] = u; char{};}))(*)[1] {return {};},
        [](auto&& u, int) -> decltype(({auto&& [x0,x1] = u; char{};}))(*)[2] {return {};},
        [](auto&& u, int) -> decltype(({auto&& [x0,x1,x2] = u; char{};}))(*)[3] {return {};},
        [](auto&& u, int) -> decltype(({auto&& [x0,x1,x2,x3] = u; char{};}))(*)[4] {return {};},
        [](auto&& u, unsigned) -> void {}
    }(declval<T>(), int{});
};

This implementation to be used only in unevaluated context and expanding out
for as many bindings as you wish, up to compiler implementation limits.

Then, you can define traits that work up to that limit:

template <typename T>
inline constexpr bool has_bindings = [] {
    if constexpr ( ! is_empty_v<T> )
        return ! is_void_v< decltype(num_bindings_impl<T>()) >;
    else return false;
}();

template <typename T>
inline constexpr unsigned num_members = [] {
    if constexpr ( ! is_empty_v<T> )
        return sizeof *num_bindings_impl<T>();
    else return 0;
}();

https://godbolt.org/z/EVnbqj

like image 78
Will Wray Avatar answered Nov 20 '22 18:11

Will Wray


The immediate situation

I was very surprised by this, as the error "raises" inside the requires-expression

To be precise, the error happens in the body of the functions you wrote. We can boil down your code to:

void function(auto& arg);

void function_with_body(auto& arg)
{
    arg.inexistent_member;
}

template<typename Arg>
concept bool test = requires(Arg arg) { function(arg); };

// never fires
static_assert( test<int> );

template<typename Arg>
concept bool test_with_body = requires(Arg arg) { function_with_body(arg); };

// never fires
static_assert( test_with_body<int> );

(on Coliru)

As far as the requires expression are concerned, the function calls are valid C++ expressions: there's nothing tricky about their return and parameter types, and the supplied argument can be passed just fine. No check is performed for the function bodies, and with good reason: the function or function template may have been declared but not defined yet (i.e. as is the case for function). Instead, it's normally up to the function template writer to make sure that the function body will be error-free for all (sensible) specializations.

Your code uses return type deduction, but that won't make too much of a difference: you can declare a function with a placeholder type (e.g. decltype(auto)) without defining it, too. (Although a call to such a function is in fact an invalid expression since the type cannot be deduced and that's visible to a requires expression, but that's not what you're doing.) In pre-concepts parlance, return type deduction is not SFINAE-friendly.

Structured bindings

As to your wider problem of writing a constraints around structured bindings, you are out of luck. As best as I know these are all the obstacles:

  • There is no interface for querying the decomposition size of a type, even though the implementation blatantly has access to the information in order to type-check structured binding declarations. (std::tuple_size is for tuple-like types only.). This could be worked around by blindingly trying sizes in increasing order though.
  • You can only ever write constraints around types and expressions, but structured bindings can only appear in normal variable declarations. (If function parameters could be structured bindings we could work around that.) No trickery with lambda expressions allowed because these can't appear in an unevaluated operand, such as in the body of a requires expression.

The closest you can go is emulate what the language is doing during an actual structured binding. That can get you support for tuple-like types (including arrays), but not 'dumb' aggregates.

(range-for is another case of a statement-level constraint that you can't fit into an expression constraint, and where emulating the language can only get you so far.)

like image 33
Luc Danton Avatar answered Nov 20 '22 18:11

Luc Danton