Is lambdification of a concept an improvement or bad practice?

Viewed 193

It appears that you can put lambda in the concept and then write code in it. Let us take this as an example. I'll prefer the standard concepts for such concepts and bear in mind that this is only for purposes of this example - godbolt

template<class T>
concept labdified_concept =
    requires {
            [](){                 
                T t, tt; // default constructible
                T ttt{t}; // copy constructible
                tt = t; //copy assignable
                tt = std::move(t); // move assignable
            };
        };

Instead of:

template<class T>
concept normal_concept = 
    std::default_initializable<T> && std::movable<T> && std::copy_constructible<T>;

Is lambdification an improvement or bad practice? From readability point too.

2 Answers

This shouldn't be valid. The point of allowed lambdas into unevaluated contexts wasn't to suddenly allow SFINAE on statements.

We do have some wording in [temp.deduct]/9 that makes this clear:

A lambda-expression appearing in a function type or a template parameter is not considered part of the immediate context for the purposes of template argument deduction. [Note: The intent is to avoid requiring implementations to deal with substitution failure involving arbitrary statements. [Example:

template <class T>
  auto f(T) -> decltype([]() { T::invalid; } ());
void f(...);
f(0);               // error: invalid expression not part of the immediate context

template <class T, std::size_t = sizeof([]() { T::invalid; })>
  void g(T);
void g(...);
g(0);               // error: invalid expression not part of the immediate context

template <class T>
  auto h(T) -> decltype([x = T::invalid]() { });
void h(...);
h(0);               // error: invalid expression not part of the immediate context

template <class T>
  auto i(T) -> decltype([]() -> typename T::invalid { });
void i(...);
i(0);               // error: invalid expression not part of the immediate context

template <class T>
  auto j(T t) -> decltype([](auto x) -> decltype(x.invalid) { } (t));   // #1
void j(...);                                                            // #2
j(0);               // deduction fails on #1, calls #2

— end example] — end note]

We just don't have something equivalent for requirements. gcc's behavior is really what you'd expect:

template <typename T> concept C = requires { []{ T t; }; };
struct X { X(int); };
static_assert(!C<X>); // ill-formed

Because the body of the lambda is outside of the immediate context, so it's not a substitution failure, it's a hard error.

Ignoring the obvious readability flaws in this mechanism, it doesn't actually work. Consider the following:

template<labdified_concept T>
void foo(T t) {}

template<typename T>
void foo(T t) {}

The rules of concepts tell us that if a given T doesn't satisfy labdified_concept, then the other foo should be instantiated instead. But that's not what happens if we provide SS to such a template. Instead, we get a hard error because labdified_concept<SS> cannot be instantiated.

The stuff within a requires expression has special handling that allows certain types of errors to be regarded as failures to meet the requirement. But that handling doesn't apply to the body of a lambda. There, ill-formed code is ill-formed and thus you get a compile error when trying to instantiate it.

And even if it did work, it still doesn't work. Concepts have complex rules for subsumption of concepts, which allows different concepts to be considered more highly specialized than others. This allows overloading on different concepts, which lets the more constrained concept get called. For example a concept that only requires default_initializable is more generic than one which requires default_initializable and moveable. Thus, if a type fulfills both, the latter will be taken because it is more constrained.

But this only works because of the special rules for concepts. Hiding requirements in lambdas wouldn't allow this to work.

Related