1

I have a function template parametrized on a member function, that accepts an object and calls different functions based on whether the return value of that member function on the object satisfies some requirement.

Example:

template<auto member> void callerFunc(S* ps)
{
    if constexpr (requires { { (ps->*member)()->f1() } -> std::same_as<int>; })
        useF1((ps->*member)());
    else
        useF2(ps);
}

This template is only used in one place, so I'd like to convert it to a lambda and put it inside the function that uses it. However, the function arguments are not dependent on the template parameter and I do not know how to express it in terms of a lambda (or whether it is even possible).

The full example is provided below and is available on compiler explorer.

The actual code is more complex, and uses classes that are generated from protobuf files (with nested messages and oneof fields) that I do not control, and macros that generate code in the switch cases.

Edit:
To clarify: in the code below, setting the caller inside the switch and calling it outside it is important, since in the actual code the switch is in a try block and the call is in the corresponding catch.

#include <concepts>
#include <iostream>

struct S1 {
    int f1() { return 1; }
    int f2() { return 2; }
};

struct S2 {
    // doesn't provide f1(), use f2() instead
    int f2() { return 2; }
};

struct S {
    S1* getS1() { return &s1; }
    S2* getS2() { return &s2; }

    S1 s1;
    S2 s2;
};

template <typename T> void useF1(T* t) { std::cout << t->f1() << "\n"; }
template <typename T> void useF2(T* t) { std::cout << t->getS2()->f2() << "\n"; }

////////////////////////////////////////////////////////////////////////////////

// How can I move this template inside Thing::foo below?
template<auto member> void callerFunc(S* ps)
{
    if constexpr (requires { { (ps->*member)()->f1() } -> std::same_as<int>; })
        useF1((ps->*member)());
    else
        useF2(ps);
}

struct Thing {
    S s;
    void foo(char c) {
        auto caller = &callerFunc<&S::getS2>; // has to be available outside the switch
        switch (c) {
            case 'a': caller = &callerFunc<&S::getS1>; break;
            case 'b': caller = &callerFunc<&S::getS2>; break;
        }
        caller(&s); // has to be outside the switch
    }
};

////////////////////////////////////////////////////////////////////////////////

int main() {
    Thing thing;
    thing.foo('a');
    thing.foo('b');
}
Alex O
  • 1,429
  • 2
  • 13
  • 20

2 Answers2

3

You can declare a template lambda and use .operator() to access its function call operator to explicitly specify the template parameter

void foo(char c) {
  auto callerFunc = []<auto member>(S* ps) {
    if constexpr (requires { { (ps->*member)()->f1() } -> std::same_as<int>; })
        useF1((ps->*member)());
      else
        useF2(ps);
    };

  switch (c) {
    case 'a': callerFunc.operator()<&S::getS1>(&s); break;
    case 'b': callerFunc.operator()<&S::getS2>(&s); break;
  }
}

If the caller must be called outside the switch case, then you can create a lambda that makes the caller, and perform type erasure by converting the returned lambda into a function pointer so that it can be reassigned according to different case values

void foo(char c) {
  auto make_caller = []<auto member> {
    return [](S* ps) {
      if constexpr (requires { { (ps->*member)()->f1() } -> std::same_as<int>; })
        useF1((ps->*member)());
      else
        useF2(ps);
      };
  };
  
  auto caller = +make_caller.operator()<&S::getS2>();
  switch (c) {
    case 'a': caller = +make_caller.operator()<&S::getS1>(); break;
    case 'b': caller = +make_caller.operator()<&S::getS2>(); break;
    }
  caller(&s);
}

Demo

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • The actual call has to be outside the switch (in the real code, it is in a `catch` clause). I will edit the question to clarify. – Alex O Jul 24 '23 at 14:23
  • @AlexO Updated. – 康桓瑋 Jul 24 '23 at 15:58
  • That's interesting. I was under the impression that a captureless lambda would convert to a function pointer automatically. Can you please explain why the `+` is needed? – Alex O Jul 24 '23 at 21:10
  • Fond this explanation: https://stackoverflow.com/questions/18889028/a-positive-lambda-what-sorcery-is-this Only the first `+` is needed – Alex O Jul 24 '23 at 21:29
  • What do you know, the code breaks clang-tidy! https://godbolt.org/z/T7vY6Ydcq – Alex O Jul 24 '23 at 21:31
  • And Clang 16 too https://godbolt.org/z/cYse4o87b – Alex O Jul 24 '23 at 21:46
  • @AlexO Looks Looks like a Clang-tidy bug. – 康桓瑋 Jul 24 '23 at 21:51
  • Yea, I reported it. Clang trunk chokes on it too. Answer's still good, even though I may not be able to use it until they fix the bug. Going to accept it, thanks for your help! – Alex O Jul 25 '23 at 02:20
2

There is a syntax to call generic lambda with non deducible template parameter with lambda.operator()<YourTemplateArg>(/*args...*/).

One way to have better calling syntax is to use deducible type helper:

So, an helper:

template <auto x>
using IC = std::integral_constant<decltype(x), x>;

so the lambda:

callerFunc = []<auto member>(S* ps, IC<member>)
{
    if constexpr (requires { { (ps->*member)()->f1() } -> std::same_as<int>; })
        useF1((ps->*member)());
    else
        useF2(ps);
};

and the call:

switch (c) {
    case 'a': caller(&s, IC<&S::getS1>{}); break;
    case 'b': caller(&s, IC<&S::getS2>{}); break;
}
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • The call has to be outside the switch. I am setting a pointer in a `try` block and calling it in the corresponding `catch` in the real code. – Alex O Jul 24 '23 at 14:25
  • You might then set a `std::variant, IC<&S::getS2>>`, and `std::visit` later... – Jarod42 Jul 25 '23 at 11:12