Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 902
NoSenseEtAl Avatar asked Apr 03 '19 06:04

NoSenseEtAl


People also ask

What is static assertion in C++?

The C++ 11 standard introduced a feature named static_assert() which can be used to test a software assertion at the compile time. Syntax : static_assert( constant_expression, string_literal ); Parameters : constant_expression : An integral constant expression that can be converted to a Boolean.

How do I initialize a Lambda as a global variable?

If we want to initialize a lambda as a global variable, we could initialize it like this: However, this is initialized at program start-up, so we can run into problems with static initialization order fiasco. One of ways to workaround this is to initialize it at compile-time instead. We could try using constexpr to do this:

What is the size passed to static assert 2?

The size passed to it is ‘2’, which fails the condition to be checked, therefore producing the compile time error, and thus halting the compilation process. What are the advantages of static_assert over #error?

What happens if false branch is not taken in lambda expression?

This works because the false branch is never taken, so just a nullptr will be returned, but the type will still be deduced as a pointer to the lambda. However, now we just have a null pointer to our lambda which in not very useful.


3 Answers

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.

like image 70
max66 Avatar answered Oct 14 '22 22:10

max66


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 :)

like image 28
olivecoder Avatar answered Oct 14 '22 22:10

olivecoder


#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.

like image 40
Martin Morterol Avatar answered Oct 14 '22 22:10

Martin Morterol