Setting type of an outer variable using auto lambda parameters

Viewed 127

I am using a C++ web framework that heavily uses callback lambda functions. As you may guess, parameters to lambdas are usually specified as auto due to the must to declare very long declarations.

Now I use decltype() operator to find the right type deduced by auto so that I can declare a vector of the same type. When the vector declaration happens inside the lambda, everything is fine.

Where my problem starts is this vector needs to be declared in an outer scope using lambdas auto parameters' type information. Below is a simple example:

std::vector<T> vec; // I want the type information to be inferred just like vec2 from lambda below
auto func = [](auto parameter){
    std::vector<decltype(parameter)> vec2; // No problem here.
};

Is this possible?

UPDATE:

The framework I am using is uWebSockets. Here is the example code :

using DataType = std::string;

// I had to go get type information from the source code.
static std::vector<uWS::WebSocket<false, true, DataType> *> users; 

uWS::App app {};

app.ws<DataType>("/*", {
    
    .open = [](auto * ws){
        // This is also doable
        // But not accessible in other lambdas.
        static std::vector<decltype(ws)> users2;
        // users2.push_back(ws);
        users.push_back(ws);
        ws->subscribe("sensors/+/house");
    },

    .close = [](auto *ws, int code, std::string_view message){
        users.erase(std::remove(users.begin(), users.end(), ws), users.end());

        // Not possible because not accessible.
        //users2.erase(std::remove(users2.begin(), users2.end(), ws), users2.end());
        std::cout << "Client disconnected!" << std::endl;
    },

    .message = [](auto *ws, std::string_view message, uWS::OpCode opCode){
        try{
            std::string message2 = std::string(message) + std::string(" ACK");
            for(const auto & ws2 : users)
                if(ws != ws2)
                    ws2->send(message2, opCode);

        }catch(std::exception& e){
            std::cout << e.what() << std::endl;
        }
    },
});

Now, nowhere in the main.cpp, there is a need to pass a parameter to lambda functions. That's where the main problem comes from.

4 Answers

The problem here is that auto has no type until it is used, auto make it a template call operator:

#include <iostream>

using namespace std;

int main()
{
    auto func = [](auto param){
        std::cout << param << std::endl;
    };
   func(4);     // integer call
   func("lol"); //const char* call 

    return 0;
}

The consequence is that param could have any arbitrary type, before we are looking at a specific usage.

If you know the argument which will be provided, then decltype can be applied:

#include <iostream>

using namespace std;

int main()
{
    int arg1 = 4;
    using Type1 = decltype(arg1); // type for the argument
    auto func = [](auto param){
        std::cout << param << std::endl;
    };
   func(arg1);  // Type1 call
   func("lol"); //const char* call 

    return 0;
}

Or maybe you could use templates.

#include <vector>

template<typename type_t>
void some_function(const type_t value) 
{
    std::vector<type_t> vec;
    auto func = [](const type_t& parameter) 
    {
        std::vector<type_t> vec2;
    };

    //....
}

auto get_something()
{
    // some made up complex data type returned as auto
    using some_complex_data_type_from_somewhere_t = std::vector<std::vector<int>>;
    some_complex_data_type_from_somewhere_t value{};
    return value;
}

int main()
{
    auto value = get_something();
    some_function(value);

    return 0;
}

You have to manually look at the type that you are initialising.

A generic lambda creates an object with a template operator(). There's not much stopping there from being multiple instantiations of that template.

If that's the case then there isn't a type that is the type of auto parameter.

You are relatively lucky here, in that the object you are initialising with all these functor members puts the arguments right in it's definition:

MoveOnlyFunction<void(WebSocket<SSL, true, UserData> *)> open = nullptr;

If you don't want to look at the definitions again, you could write a trait

template<typename Signature>
struct MoveOnlyFunctionTraits;

template <typename R, typename... Arguments>
struct MoveOnlyFunctionTraits<MoveOnlyFunctionTraits<R(Arguments...)> {
    using result_type = R;

    static constexpr auto arity = sizeof...(Arguments);

    template <std::size_t I>
    using argument = std::tuple_element_t<std::tuple<Arguments...>, I>;
}

template<typename Signature, std::size_t I>
using argument_t = typename MoveOnlyFunctionTraits<Signature>::template argument<I>;

That allows you to pull out the type

using websocket_type = argument_t<decltype(uWS::App::WebSocketBehaviour<DataType>::open), 0>;
std::vector<websocket_type> users;

Or you could submit a pull request to the library, that exposes type aliases you are interested in

template <bool SSL>
struct TemplatedApp {
    // existing members ...
public:
    template <typename DataType>
    using websocket_type = WebSocket<SSL, true, DataType>;
};

Then

std::vector<uWS::App::template websocket_type<DataType> *> users;

You can "front-load" the type inference into a type alias.

void some_function() {
  using value_type = decltype(infer_from_somewhere);
  // alt: using value_type = typename some::annoyingly<complex>::type;

  std::vector<value_type> vec;
  auto func = [](value_type parameter){
    std::vector<value_type> vec2;
  };

  //...
}
Related