为非数组重载 std::begin() 和 std::end()

Overloading std::begin() and std::end() for non-arrays

假设我有以下 Data class:

struct Data {
   char foo[8];
   char bar;
};

和下面的函数,my_algorithm,需要一对char *(类似于STL算法):

void my_algorithm(char *first, char *last);

对于Datafoo数据成员,而不是像这样调用my_algorithm()

Data data;
my_algorithm(data.foo, data.foo + 8);

我可以使用 std::begin() and std::end() 便捷函数模板:

my_algorithm(std::begin(data.foo), std::end(data.foo));

我想实现类似于 Databar 数据成员的东西。也就是说,而不是写:

my_algorithm(&data.bar, &data.bar + 1);

我想写这样的东西:

my_algorithm(begin(data.bar), end(data.bar));

因此,我为这种情况定义了以下两个普通(非模板)函数:

char* begin(char& c) { return &c; }
char*   end(char& c) { return &c + 1; }

这样我就可以编写如下代码:

Data data;

using std::begin;
using std::end;

my_algorithm(begin(data.foo), end(data.foo)); // ok - std::begin()/std::end()
my_algorithm(begin(data.bar), end(data.bar)); // Error!!!

使用上面的 using 声明,我预计 std::begin()/std::end()::begin()/::end() 分别位于同一个重载集中.由于函数 ::begin()::end() 是后一个调用的完美匹配,而且它们不是模板,所以我希望最后一次调用 my_algorithm() 能够匹配它们。但是,根本不考虑普通功能。结果编译失败,因为 std::begin()std::end() 与调用不匹配。

基本上,后一个调用就像我写的一样:

my_algorithm(begin<>(data.bar), end<>(data.bar));

也就是说,重载决策过程只考虑函数模板(即std::begin()/std::end()),而不考虑普通函数(即不::begin()/::end()).

如果我完全限定对 ::begin()/::end():

的调用,它只会按预期工作
my_algorithm(::begin(data.bar), ::end(data.bar));

我在这里错过了什么?

问题的标题是“Overloading std::begin()”。重载只能在同一范围内进行。那就是你不能重载来自不同范围的名称。在另一个范围内,我们只能努力帮助查找名称。本质上,这里 "using std::begin" 声明隐藏了 ::begin 问题代码。参考S.Lippman:

functions that are members of two distinct namespaces do not overload one another.

Scope of a using Declaration. Names introduced in a using declaration obey normal scope rules. Entities with the same name defined in an outer scope are hidden.

只要参数是 char 并且 char 是基本类型 - 不应考虑参数依赖查找 - 如评论中所述 - 没有与基本类型相关联的命名空间。 同样,问题是:"What am I missing?" - 因此答案仅关注原因 - 建议可能过于宽泛。

让我们得到一个完整的、可重现的例子:

#include <iterator>

char* begin(char& c) { return &c; }
char*   end(char& c) { return &c + 1; }

namespace ns {
    void my_algorithm(char *first, char *last);

    void my_function() {
        using std::begin;
        using std::end;

        char c = '0';
        my_algorithm(begin(c), end(c));
    }
}

当您对 begin(c)end(c) 进行非限定调用时,编译器会经历 unqualified name lookup (described on the Argument-dependent lookup page of cppreference).

的过程

对于常规的非限定名称查找,该过程大致是从您当前所在的命名空间开始——::ns 在这种情况下——只有在找不到特定名称时才移出一个命名空间。

如果一个函数调用是不合格的,就像这里的 begin(c)end(c)argument dependent lookup 可能会发生,它会找到在与类型相同的名称空间中声明的自由函数函数的参数,通过查找 "associated namespaces."

扩展重载集的过程

但是,在这种情况下,char 是基本类型,因此依赖于参数的查找不允许我们找到全局 ::begin::end 函数。

For arguments of fundamental type, the associated set of namespaces and classes is empty

cppreference: argument dependent lookup

相反,因为我们已经有了 using std::begin; using std::end;,编译器已经看到了 begin(...)end(...) 的可能函数——即那些定义在命名空间 ::std 中的函数——而不必将命名空间从 ::ns 移出到 ::。因此,编译器使用这些函数,编译失败。


值得注意的是,using std::begin; using std::end; 也会阻止编译器找到自定义 ::begin::end,即使您将它们放在 ::ns 中也是如此。


您可以改为编写自己的 beginend:

#include <iterator>

namespace ns {
    char* begin(char& c) { return &c; }
    char*   end(char& c) { return &c + 1; }

    template <typename T>
    auto begin(T&& t) {
        using std::begin;
        // Not unbounded recursion if there's no `std::begin(t)`
        // or ADL `begin(t)`, for the same reason that our
        // char* begin(char& c); overload isn't found with
        // using std::begin; begin(c);
        return begin(t);
    }

    template <typename T>
    auto end(T&& t) {
        using std::end;
        return end(t);
    }

    void my_algorithm(char *first, char *last);

    void my_function() {
        char c = '0';
        my_algorithm(ns::begin(c), ns::end(c));
    }
}