Home > Net >  Template type inference using std::views
Template type inference using std::views

Time:11-25

I am coming somewhat belatedly to Functional Programming, and getting my head around ranges/views. I'm using MSVC19 and compiling for C 20.

I'm using std::views::transform and the compiler doesn't seem to be inferring type as I might naively hope.

Here's a small example, which simply takes a vector of strings and computes their length:

#include <vector>
#include <iostream>
#include <ranges>

template<typename E>
auto length(const E& s)
{
    std::cout << "Templated length()\n";
    return static_cast<int>(s.length());
}

template<typename E>
auto getLengths(const std::vector<E>& v)
{
    return  v | std::views::transform(length<E>);
}

int main()
{
    std::vector<std::string> vec = { "Larry","Curly","Moe" };
    for (int i : getLengths(vec))
    {
        std::cout << i << "\n";
    }
    return 0;
}

with the output:

Templated length()
5
Templated length()
5
Templated length()
3

My question is why does changing the code in this line (dropping the <E>):

    return  v | std::views::transform(length);

give me an armful of errors, starting with: Error C2672 'operator __surrogate_func': no matching overloaded function found ?

Why doesn't the compiler infer that the type is std::string? If I replace the templates with a non-templated function:

auto length(const std::string& s) -> int
{
    std::cout << "Specialized length()\n";
    return static_cast<int>(s.length());
}

The code compiles and runs, so clearly without the template, the compiler finds a match for the particular type I am using.

CodePudding user response:

This has nothing to do with views. You can reduce the problem to:

template <typename T>
int length(T const& x) { return x.length(); }

template <typename F>
void do_something(F&& f) {
   // in theory use f to call something
}

void stuff() {
    do_something(length); // error
}

C doesn't really do type inference. When you have do_something(length), we need to pick which length we're talking about right there. And we can't do that, so it's an error. There's no way for do_something to say "I want the instantiation of the function template that will be called with a std::string - it's entirely up to the caller to give do_something the right thing.

The same is true in the original example. length<E> is a concrete function. length is not something that you can just pass in.

The typical approach is to delay instantiation by wrapping your function template in a lambda:

void stuff() {
    do_something([](auto const& e) { return length(e); }); // ok
}

Now, this works - because a lambda is an expression that has a type that can be deduced by do_something, while just length is not. And we don't have to manually provide the template parameter, which is error prone.

We can generalize this with a macro:

#define FWD(arg) static_cast<decltype(arg)&&>(arg)
#define LIFT(name) [&](auto&&... args) -> decltype(name(FWD(args)...)) { return name(FWD(args)...); }

void stuff() {
    do_something(LIFT(length));
}

Which avoids some extra typing and probably makes the intent a little clearer.

  • Related