Search code examples
c++c++11templatesoverloadingusing-declaration

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


Let's assume I have the following Data class:

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

and the following function, my_algorithm, which takes a pair of char * (similar to an STL algorithm):

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

For Data's foo data member, instead of calling my_algorithm() like this:

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

I can use the std::begin() and std::end() convenience function templates:

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

I would like to achieve something similar to Data's bar data member. That is, instead of writing:

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

I would like to write something like:

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

Therefore, I've defined the two following ordinary (non-template) functions for this case:

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

So that I would be able to write code like the following:

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!!!

With the using declarations above I would have expected std::begin()/std::end() and ::begin()/::end() to be in the same overload set, respectively. Since the functions ::begin() and ::end() are a perfect match for the latter call and they are not templates, I would have expected the last call to my_algorithm() to match them. However, the ordinary functions are not considered at all. As a result the compilation fails, because std::begin() and std::end() are not matches for the call.

Basically, the latter call acts as if I had written instead:

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

That is, only the function templates (i.e., std::begin()/std::end()) are considered by the overload resolution process, not the ordinary functions (i.e., not ::begin()/::end()).

It only works as expected, if I fully qualify the calls to ::begin()/::end():

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

What am I missing here?


Solution

  • Let's get a complete, reproducible example:

    #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));
        }
    }
    

    When you make the unqualified call to begin(c) and end(c), the compiler goes through the process of unqualified name lookup (described on the Argument-dependent lookup page of cppreference).

    For regular unqualified name lookup, the process is roughly to start at the namespace you are currently in—::ns in this case—and only move out a namespace if you don't find the specific name.

    If a function call is unqualified, as it is here with begin(c) and end(c), argument dependent lookup can occur, which finds free functions declared in the same namespace as the types of the functions' arguments, through the process of extending the overload set by finding "associated namespaces."

    In this case, however, char is a fundamental type, so argument dependent lookup doesn't allow us to find the global ::begin and ::end functions.

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

    cppreference: argument dependent lookup

    Instead, as we already have using std::begin; using std::end;, the compiler already sees possible functions for begin(...) and end(...)—namely those defined in namespace ::std—without having to move out a namespace from ::ns to ::. Thus, the compiler uses those functions, and compilation fails.


    It's worth noting that the using std::begin; using std::end; also block the compiler from finding the custom ::begin and ::end even if you were to place them inside ::ns.


    What you can do instead is write your own begin and end:

    #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));
        }
    }