为何transform_view无法识别作为参数的仿函数?
问题背景
原本认为以下两个表达式等价:
R | std::views::transform([](auto&& e) { return f(e); }); R | std::views::transform(f); // 更简洁,无需多余Lambda
但实际遇到反例,相关代码如下:
#include <generator> #include <map> #include <ranges> std::generator<int> to_prime_factors(int x) { for (int i = 2; i < x; ++i) { while (x % i == 0) { co_yield i; x /= i; } } if (x > 1) { co_yield x; } } template <std::ranges::input_range R> std::map<std::ranges::range_value_t<R>, int> to_counter(R&& range) { std::map<std::ranges::range_value_t<R>, int> counter; for (auto&& ele : range) { counter[ele]++; } return counter; } int main() { static_assert( std::same_as<int, std::ranges::range_value_t<std::generator<int>>>); auto as = std::views::iota(2, 6); as | std::views::transform(to_prime_factors) | std::views::transform([](auto&& factors) { static_assert(std::ranges::input_range<decltype(factors)>); return to_counter(factors); // stinky unnecessary lambda }); as | std::views::transform(to_prime_factors) | std::views::transform(to_counter); // oops return 0; }
编译时GCC提示最后一个表达式参数数量不匹配,无法识别为部分适配器,且提示Args = {},与预期不符。
技术解答
1. 为何Lambda包装后编译结果不同?
std::views::transform要求传入的可调用对象必须能被单个参数调用。这里的核心问题是:模板函数to_counter本身不是一个可调用对象——它是一个模板,必须先实例化出具体的函数版本才能被调用。
使用Lambda[](auto&& e) { return to_counter(e); }时,Lambda的调用运算符是一个模板,会在transform调用它时自动推导参数类型,进而实例化出匹配的to_counter版本。而直接传递to_counter时,transform无法自行推导应该实例化哪个版本的to_counter,因为它需要的是一个确定的可调用实体,而非模板本身。
简言之:Lambda是具体的可调用对象,能处理参数推导并触发模板实例化;to_counter作为模板名,本身不具备可调用性,无法直接被transform使用。
2. 为何GCC会提示参数匹配异常?
不是GCC忽略了参数,而是std::views::transform的适配器机制无法正确处理模板名。transform需要接收一个能接收单个参数的可调用对象,但to_counter是模板函数名,它代表的是一组函数而非某个具体函数。当直接传递模板名时,编译器无法确定要实例化哪个具体函数,也就无法将其作为有效参数传递给transform。
此时编译器尝试将transform(to_counter)视为部分应用,但由于模板名不满足“可被单个参数调用”的要求,最终报错提示参数数量不匹配。
内容的提问来源于stack exchange,提问作者haleyk

