10

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.

max66
  • 65,235
  • 10
  • 71
  • 111
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • 1
    What if the functor has an overloaded `operator()`? Visiting is also very commonly performed with overloaded functors (see example 4 [here](https://en.cppreference.com/w/cpp/utility/variant/visit)), do those have to be forbidden (or have to work)? – Max Langhof Apr 03 '19 at 07:35
  • I think that is too hard to handle, but if it can be done that would be nice... so it is optional, not required. – NoSenseEtAl Apr 03 '19 at 08:16

3 Answers3

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.

max66
  • 65,235
  • 10
  • 71
  • 111
  • 2
    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: https://godbolt.org/z/M319jo – Max Langhof Apr 03 '19 at 08:32
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 :)

olivecoder
  • 2,858
  • 23
  • 22
  • Nice idea ! You may ensure that "Auto" is unique like that : `auto a_lambda = [](){}; using Auto= decltype(a_lambda);` – Martin Morterol Apr 03 '19 at 09:58
  • @Martinm This seems to be the idiomatic way but does it actually have an advantage over the code in this answer? – Konrad Rudolph Apr 03 '19 at 10:07
  • 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. – Martin Morterol Apr 03 '19 at 10:19
  • 4
    This gives both false positives (e.g. `Visit([](std::any){});`) and false negatives (`Visit([](int, auto){});` or `Visit([](auto*){});`) – Barry Apr 03 '19 at 11:47
  • `Visit([](int, auto){})` is not a valid case because of `v(std::get(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. – olivecoder Apr 03 '19 at 12:09
  • I can give more examples if you want. `Visit([](auto, int=0){})` or `Visit([](auto*&){})` or, in C++20, `Visit([](Integral auto){})` or `Visit([](vector){})` or ... – Barry Apr 03 '19 at 14:35
  • 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. – Barry Apr 03 '19 at 14:37
  • Or C++17-style "concepts" `[](auto x) -> enable_if_t> {}` – Barry Apr 03 '19 at 14:45
  • 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. – NoSenseEtAl Apr 03 '19 at 23:46
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.

Martin Morterol
  • 2,560
  • 1
  • 10
  • 15