Is Lambdification of a Concept an Improvement or Bad Practice

Is lambdification of a concept an improvement or bad practice?

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.

SFINAE not always works in C++?

I think problem is that you try SFINAE based error in body of lambda.
And if I understand this is far to late to recovery from error.

Base idea of SFINAE is to remove incompatible function from overload set, not to to recover from compilation failures, line is bit blurred,
but probably best rule of thumb is that error need happen only in function declaration.

e.g.


template<typename T>
struct Get
{
using type = typename T::type;
};

void F1(...){}
template<typename T, typename TT = typename Get<T>::type>
void F1(T i){}

void F2(...){}
template<typename T, typename TT = typename T::type>
void F2(T i){}

int main() {
//F1(3); //hard error
F2(3); //SFINAE - work fine
}

Type Get<T> fail to be created, in theory compiler could recover from this but this could be costly (image error happens in deep hierarchy).
F2 fail correctly because compiler only need to look on function header to see if it is correct.

If you could move required check to template header then it could in theory work. Something like:


template <typename T>
void Test() {
std::cout << Comp<T>(
[](auto Ts, decltype((*(std::stringstream*)0) << (*(typename decltype(Ts)::template type<0>*)0))* = {})
{
typename decltype(Ts)::template type<0> * p = 0;
std::stringstream ss; ss << (*p);
}
) << std::endl;
}

Second lambda parameter is evaluated on call site and can "visibly" fails applying arguments.
Its not give results as you want but it compile, probably you would need update pram type to correctly reflect operation in lambda.

C++20 Concepts : Which template specialization gets chosen when the template argument qualifies for multiple concepts?

This is because concepts can be more specialized than others, a bit like how template order themselves. This is called partial ordering of constraints

In the case of concepts, they subsumes each other when they include equivalent constraints. For example, here's how std::integral and std::signed_integral are implemented:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T> // v--------------v---- Using the contraint defined above
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Normalizing the constraints the compiler boil down the contraint expression to this:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T>
concept signed_integral = std::is_integral_v<T> && std::is_signed_v<T>;

In this example, signed_integral implies integral completely. So in a sense, a signed integral is "more constrained" than an integral.

The standard writes it like this:

From [temp.func.order]/2 (emphasis mine):

Partial ordering selects which of two function templates is more
specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type.
The deduction process determines whether one of the templates is more specialized than the other.
If so, the more specialized template is the one chosen by the partial ordering process.
If both deductions succeed, the partial ordering selects the more constrained template as described by the rules in [temp.constr.order].

That means that if there is multiple possible substitution for a template and both are choosen from partial ordering, it will select the most constrained template.

From [temp.constr.order]/1:

A constraint P subsumes a constraint Q if and only if, for every disjunctive clause Pi in the disjunctive normal form of P, Pi subsumes every conjunctive clause Qj in the conjunctive normal form of Q, where

  • a disjunctive clause Pi subsumes a conjunctive clause Qj if and only if there exists an atomic constraint Pia in Pi for which there exists an atomic constraint Qjb in Qj such that Pia subsumes Qjb, and

  • an atomic constraint A subsumes another atomic constraint B if and only if A and B are identical using the rules described in [temp.constr.atomic].

This describe the subsumption algorithm that compiler use to order constraints, and therefore concepts.

How to add a callable function to a Sympy expression

One way but it requires hard-coding the function to be called in the class:

f2 = lambda t: np.sin(t)

class MyFunc(Function):
@classmethod
def eval(cls, arg):
arg = sympify(arg, strict=True)
if arg.is_Number:
return sympify(f2(float(arg)), strict=True)

More like Davide's answer but with a couple of fixes:

class FuncWrapper(Symbol):
"""Wraps a python callable as a Basic instance"""
def __new__(cls, func, name):
obj = super().__new__(cls, name)
obj._wrapped = func
return obj

@property
def wrapped(self):
return self._wrapped

def _hashable_content(self):
return (self.wrapped,) # needed for __eq__

def eval(self, arg):
if arg.is_Number:
return sympify(self.wrapped(float(arg)))

def __call__(self, arg):
return Call(self, arg)

class Call(Function):
@classmethod
def eval(cls, func, arg):
arg = sympify(arg)

result = func.eval(arg)
if result is not None:
return result

With that you have:

In [61]: f = FuncWrapper(np.sin, 'f')

In [62]: x + f(x)
Out[62]: x + Call(f, x)

In [63]: _.subs(x, 1)
Out[63]: 1.84147098480790


Related Topics



Leave a reply



Submit