为什么我们不能在某些 STL 容器中不提及其类型而直接使用 lambda?

Why can't we directly use lambdas without mentioning its type in some STL containers?

我正在使用 STL 容器,试图实现自定义 comparators/predicates,我意识到当将谓词作为 Functor 提供时,所有容器的语法似乎都相同,即您只需提及仿函数作为模板参数中的相应参数,bam 它有效!

现在来到 Lambdas,它们在 std::vector 中的容器中工作得很好,但在 std::set 中却不行,语法真的很奇怪。这是一些与此相关的代码

对于 std::vector 这很好用

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> a = { 3, 1, 0, 2 };
    sort(a.begin(), a.end(), [](const int& num1, const int& num2) -> bool {
        return num1 < num2;
    });

    for (const int& num : a)
    {
        cout << num << " ";
    }
}

但是对于 std::set 这不起作用

auto PredicateLambda = [](const pair<int, string>& p1, const pair<int, string>& p2) -> bool {
    if (p1.first != p2.first)
        return p1.first < p2.first;
    else
        return p1.second < p2.second;
};

set<pair<int, string>, PredicateLambda> st;

相反,您将不得不做

set<pair<int, string>, decltype(PredicateLambda)> st(PredicateLambda);

我觉得这真的很奇怪,因为类型是在编译时确定的,手动指定似乎没有必要。我觉得我在这里遗漏了一些东西,如果有人可以填补为什么这样做的空白,我真的很感激?

P.S。我想到的一个解释是对谓词的类型进行了某种静态时间检查,但是,这让我想到了另一个问题,即在 Functors 的情况下如何进行类型检查,这似乎正常工作(即仅指定仿函数的名称)


编辑:问题的目的不是要了解语法,而是设计背后的推理。原因是 std::sort() 本质上只需要一个可调用对象,它可以比较两个值并使用它进行就地排序,而 std::set class 必须在其 [=42] 中声明谓词=] 因此也需要对象的类型。当我们给出 Functor 的名称时,我们实际上是在给出 Class 名称(就像一个类型),但是 PredicateLambda 是一个对象,因此我们使用 decltype 来表示它的类型。

您混淆了函数模板和 class 模板。对于这两种情况,要出现任何代码,都必须对其进行实例化。让我们看看每个案例:

std::sort is a function template which accepts the binary-predicate (i.e. callable compare function/ function objects)作为其第三个参数。

template< class RandomIt, class Compare >
constexpr void sort( RandomIt first, RandomIt last, Compare comp ); (since C++20)
                                                    ^^^^^^^^^^^^

这里编译器会尝试从传递的函数参数中确定模板参数类型(即RandomItCompare),即所谓的隐式实例化如果您没有明确提及它们,将自动发生。

这意味着,也可以使用模板参数调用 std::sort(显式实例化):

const auto compare = [](const int& num1, const int& num2) { ...
std::sort<std::vector<int>::iterator, decltype(compare)>(a.begin(), a.end(), compare);
//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

std::set is a class template。只要不在此处指定模板参数,就不会得到具体的类型。

A class template by itself is not a type, or an object, or any other entity. No code is generated from a source file that contains only template definitions. In order for any code to appear, a template must be instantiated: the template arguments must be provided so that the compiler can generate an actual class (or function, from a function template).

template<
    class Key,
    class Compare = std::less<Key>,         // optional
    class Allocator = std::allocator<Key>
> class set;

此处,Compare 是可选的,但它是 std::set 的 class 模板类型的一部分,可以用用户定义的比较函数/可调用函数替换。如果不提供可调用的类型,编译器无法实例化您想要的 std::set 版本。


进一步阅读

  • std::pair has compare operations defined。因此,您的示例代码中不需要 PredicateLambda 。我希望它是为了演示目的。

  • 因为 we have deduction guide,对于模板class,可以简单地写成:

    #include <set>
    #include <tuple>
    #include <string>
    using namespace std::string_literals;
    
    std::set st{ std::pair{1, "string"s} }; // st uses the 
    
    

    也可以自己提供deduction guides

  • 因为 lambda 表达式是默认可构造的。因此, 在较新的版本中,您可能不需要将 PredicateLambda 作为参数传递给 st 编译器。

    std::set<std::pair<int, std::string>, decltype(PredicateLambda)> st;
    

模板classes和函数的类型推导规则不同。

您正在看到 std::sort 推导其参数,包括谓词、类型。

与此同时,std::set class 模板类型推导被阻止,因为您为其提供了模板参数。

中您可以使用演绎指南:

std::set s( {{1, "hello"s}}, PredicateLambda );

其中 PredicateLambda 的类型被移入集合。

回到你可以使用函数指针

std::set<std::pair<int, std::string>, bool(*)(std::pair<int, std::string> const&)> s(
  {{1, "hello"}}, PredicateLambda );

这依赖于无状态 lambda 可以隐式转换为具有匹配签名的函数指针这一事实。

如果你的 predicate 是 stateful(即捕获东西),你可以在 std::set 的类型中使用 std::function<bool(std::pair<int, std::string> const&)> 来存储 predicate 和不公开其类型。


现在,在 中,我们可以使您的语法更简单一些,如下所示:

#include <set>
#include <string>

auto PredicateLambda = [](auto& p1, auto& p2) -> bool {
    if (p1.first != p2.first)
        return p1.first < p2.first;
    else
        return p1.second < p2.second;
};

template<auto x>
struct call_t {
  decltype(auto) operator()(auto&&...args)const {
    return x(decltype(args)(args)...);
  }
};


int main() {
  std::set<std::pair<int, std::string>, call_t<PredicateLambda>> st;
  st.insert( {0, "hello"} );
}

现在我们只提一次模板名称,不再使用decltype

Live example.