Is it possible to static_assert that a lambda is not generic?
Asked Answered
R

3

11

I implemented a Visit function (on a variant) that checks that the currently active type in the variant matches the function signature (more precisely the first argument). Based on this nice answer. For example

#include <variant>
#include <string>
#include <iostream>

template<typename Ret, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(*) (Arg, Rest...));

template<typename Ret, typename F, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(F::*) (Arg, Rest...));

template<typename Ret, typename F, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(F::*) (Arg, Rest...) const);

template <typename F>
decltype(first_argument_helper(&F::operator())) first_argument_helper(F);

template <typename T>
using first_argument = decltype(first_argument_helper(std::declval<T>()));

std::variant<int, std::string> data="abc";
template <typename V>
void Visit(V v){
using Arg1 = typename std::remove_const_t<std::remove_reference_t<first_argument<V>>>;//... TMP magic to get 1st argument of visitor + remove cvr, see Q 43526647
if (! std::holds_alternative<Arg1>(data)) {
std::cerr<< "alternative mismatch\n";
return;
}
v(std::get<Arg1>(data));
}
int main(){
    Visit([](const int& i){std::cout << i << "\n"; });
    Visit([](const std::string& s){std::cout << s << "\n"; });
    // Visit([](auto& x){}); ugly kabooom
}

This works, but it explodes with a user unfriendly compile time error when users passes a generic (e.g. [](auto&){}) lambda. Is there a way to detect this and give nice static_assert() about it? Would also be nice if it worked with function templates as well, not just with lambdas.

Note that I do not know what possible lambdas do, so I can not do some clever stuff with Dummy types since lambdas may invoke arbitrary functions on types. In other words I can not try to call lambda in 2 std::void_t tests on int and std::string and if it works assume it is generic because they might try to call .BlaLol() on int and string.

Rhody answered 3/4, 2019 at 6:21 Comment(2)
What if the functor has an overloaded operator()? Visiting is also very commonly performed with overloaded functors (see example 4 here), do those have to be forbidden (or have to work)?Valery
I think that is too hard to handle, but if it can be done that would be nice... so it is optional, not required.Rhody
T
11

Is there a way to detect this and give nice static_assert about it?

I suppose you can use SFINAE over operator() type.

Follows an example

#include <type_traits>

template <typename T>
constexpr auto foo (T const &)
   -> decltype( &T::operator(), bool{} )
 { return true; }

constexpr bool foo (...)
 { return false; }

int main()
 {
   auto l1 = [](int){ return 0; };
   auto l2 = [](auto){ return 0; };

   static_assert( foo(l1), "!" );
   static_assert( ! foo(l2), "!" );
 }

Instead of a bool, you can return std::true_type (from foo() first version) or std::false_type (from second version) if you want to use it through decltype().

Would also be nice if it worked with function templates as well, not just with lambdas.

I don't think it's possible in a so simple way: a lambda (also a generic lambda) is an object; a template function isn't an object but a set of objects. You can pass an object to a function, not a set of objects.

But the preceding solution should works also for classes/structs with operator()s: when there is a single, non template, operator(), you should get 1 from foo(); otherwise (no operator(), more than one operator(), template operator()), foo() should return 0.

Tung answered 3/4, 2019 at 8:6 Comment(1)
Trying to take the address of operator() was my initial idea as well (hence the comment) but then I somehow got lost in SFINAE on actually calling it. Anyway, here are some extra test cases involving overloaded functors: godbolt.org/z/M319joValery
C
4

Yet another simpler option:

#include <type_traits>
...
template <typename V>
void Visit(V v) {
   class Auto {};
   static_assert(!std::is_invocable<V, Auto&>::value);
   static_assert(!std::is_invocable<V, Auto*>::value);
   ...
}

The Auto class is just an invented type impossible to occur in the V parameters. If V accepts Auto as an argument it must be a generic.

I tested in coliru and I can confirm the solution covers these cases:

Visit([](auto x){}); // nice static assert
Visit([](auto *x){}); // nice static assert
Visit([](auto &x){}); // nice static assert
Visit([](auto &&x){}); // nice static assert

I'm not sure if that would cover all the possible lambdas that you don't know which are :)

Clapboard answered 3/4, 2019 at 8:56 Comment(9)
Nice idea ! You may ensure that "Auto" is unique like that : auto a_lambda = [](){}; using Auto= decltype(a_lambda);Evante
@Martinm This seems to be the idiomatic way but does it actually have an advantage over the code in this answer?Butylene
Well, I am not sure what happens in the case where an "Auto" class already define somewhere else. In meta-programming context I prefer to be sure that my type cannot be in conflict in some obscure case.Evante
This gives both false positives (e.g. Visit([](std::any){});) and false negatives (Visit([](int, auto){}); or Visit([](auto*){});)Zaffer
Visit([](int, auto){}) is not a valid case because of v(std::get<Arg1>(data)); and Visit([](auto*){}); is fixed now. I'm not sure if Visit([](std::any){}) is one of the cases to be avoided as the question isn't clear enough.Clapboard
I can give more examples if you want. Visit([](auto, int=0){}) or Visit([](auto*&){}) or, in C++20, Visit([](Integral auto){}) or Visit([]<class T>(vector<T>){}) or ...Zaffer
Admittedly it's a little weird to put an any into a variant, but it's certainly possible - so having a check that prevents it from being used seems questionable.Zaffer
Or C++17-style "concepts" [](auto x) -> enable_if_t<is_integral_v<decltype(x)>> {}Zaffer
from my question "Note that I do not know what possible lambdas do, so I can not do some clever stuff with Dummy types since lambdas may invoke arbitrary functions on types." So I assume your solution would disqualify ok lambdas that call functions on Auto that auto does not have.Rhody
E
3
#include <variant>
#include <string>
#include <iostream>

template <class U, typename T = void>
struct can_be_checked : public std::false_type {};

template <typename U>
struct can_be_checked<U, std::enable_if_t< std::is_function<U>::value > >  :  public std::true_type{};

template <typename U>
struct can_be_checked<U, std::void_t<decltype(&U::operator())>> :  public std::true_type{};


template<typename Ret, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(*) (Arg, Rest...));

template<typename Ret, typename F, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(F::*) (Arg, Rest...));

template<typename Ret, typename F, typename Arg, typename... Rest>
Arg first_argument_helper(Ret(F::*) (Arg, Rest...) const);

template <typename F>
decltype(first_argument_helper(&F::operator())) first_argument_helper(F);

template <typename T>
using first_argument = decltype(first_argument_helper(std::declval<T>()));

std::variant<int, std::string> data="abc";


template <typename V>
void Visit(V v){
    if constexpr ( can_be_checked<std::remove_pointer_t<decltype(v)>>::value )
    {
        using Arg1 = typename std::remove_const_t<std::remove_reference_t<first_argument<V>>>;//... TMP magic to get 1st argument of visitor + remove cvr, see Q 43526647
        if (! std::holds_alternative<Arg1>(data)) 
        {
            std::cerr<< "alternative mismatch\n";
            return;
        }
        v(std::get<Arg1>(data));
    }
    else
    {
        std::cout << "it's a template / auto lambda " << std::endl;
    }


}

template <class T>
void foo(const T& t)
{
    std::cout <<t << " foo \n";
}

void fooi(const int& t)
{
    std::cout <<t << " fooi " << std::endl;
}

int main(){
    Visit([](const int& i){std::cout << i << std::endl; });
    Visit([](const std::string& s){std::cout << s << std::endl; });
    Visit([](auto& x){std::cout <<x << std::endl;}); // it's a template / auto lambda*/
    Visit(foo<int>);

    Visit<decltype(fooi)>(fooi);
    Visit(fooi);                 


    // Visit(foo); // => fail ugly
}

I don't know if it's you want, but you can, with that static_assert if an auto lambda is passed as parameter.

I think it's not possible to do the same for template function, but not sure.

Evante answered 3/4, 2019 at 8:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.