Invoking begin and end via using-directive?
Asked Answered
A

4

16

The established idiom for invoking swap is:

using std::swap
swap(foo, bar);

This way, swap can be overloaded for user-defined types outside of the std namespace.

Should we invoke begin and end in the same fashion?

using std::begin;
using std::end;
some_algorithm(begin(some_container), end(some_container));

Or should we just write:

some_algorithm(std::begin(some_container), std::end(some_container));
Antimonic answered 13/9, 2013 at 7:9 Comment(3)
sorry, I think this is a dupe https://mcmap.net/q/325122/-should-custom-containers-have-free-begin-end-functions/819272Herbalist
Who decides what the established idioms are?While
@NeilKirk Books like Effective C++Antimonic
Y
9

Using a using-declaration like that is the correct way IMO. It's also what the standard does with the range for loop: if there is no begin or end members present then it will call begin(x) and end(x) with std as an associated namespace (i.e. it will find std::begin and std::end if ADL doesn't find non-member begin and end).

If you find that writing using std::begin; using std::end; all the time is tedious then you can use the adl_begin and adl_end functions below:

namespace aux {

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

template<class T>
auto adl_begin(T&& x) -> decltype(begin(std::forward<T>(x)));

template<class T>
auto adl_end(T&& x) -> decltype(end(std::forward<T>(x)));

template<class T>
constexpr bool is_array()
{
    using type = typename std::remove_reference<T>::type;
    return std::is_array<type>::value;
}

} // namespace aux

template<class T,
         class = typename std::enable_if<!aux::is_array<T>()>::type>
auto adl_begin(T&& x) -> decltype(aux::adl_begin(std::forward<T>(x)))
{
    using std::begin;
    return begin(std::forward<T>(x));
}

template<class T,
         class = typename std::enable_if<!aux::is_array<T>()>::type>
auto adl_end(T&& x) -> decltype(aux::adl_end(std::forward<T>(x)))
{
    using std::end;
    return end(std::forward<T>(x));
}

template<typename T, std::size_t N>
T* adl_begin(T (&x)[N])
{
    return std::begin(x);
}

template<typename T, std::size_t N>
T* adl_end(T (&x)[N])
{
    return std::end(x);
}

This code is pretty monstrous. Hopefully with C++14 this can become less arcane:

template<typename T>
concept bool Not_array()
{
    using type = std::remove_reference_t<T>;
    return !std::is_array<type>::value;
}

decltype(auto) adl_begin(Not_array&& x)
{
    using std::begin;
    return begin(std::forward<Not_array>(x));
}

decltype(auto) adl_end(Not_array&& x)
{
    using std::end;
    return end(std::forward<Not_array>(x));
}

template<typename T, std::size_t N>
T* adl_begin(T (&x)[N])
{
    return std::begin(x);
}

template<typename T, std::size_t N>
T* adl_end(T (&x)[N])
{
    return std::end(x);
}
Yoohoo answered 13/9, 2013 at 7:22 Comment(5)
@Useless What you'd expect: it returns the type returned by begin(T). The using-declarations that bring std::begin and std::end into scope only kick in if ADL doesn't find non-member begin or end.Yoohoo
How is this better than writing 1 more character for std::begin?While
@NeilKirk because std::begin will do the wrong thing sometimes.Yoohoo
They should fix the language. This is what puts people off C++.While
@NeilKirk I agree it should be fixed in some way (I got tripped up by two edge cases writing this). Hopefully with C++14 concepts it becomes easier to deal with (solution at end).Yoohoo
H
6

Disclaimer: For the pedantic types (or pedants, if you want to be pedantic...), I generally refer to the word "overload" here as "Create functions that have the names begin and end and do using std::begin; using std::end;.", which, believe me, is not tedious for me to write at all, but is very hard to read and is redundant to read. :p.


I'll basically give you the possible use-cases of such technique, and later my conclusion.

Case 1 - Your begin and end methods do not act like those of the standard containers

One situation where you may need to overload the std::begin and std::end functions is when you're using the begin and end methods of your type in a different way other than to provide iterator-like access to the elements of an object, and want to have overloads of std::begin and std::end call the begin and end methods used for iteration.

struct weird_container {
   void begin() { std::cout << "Start annoying user." }
   void end() { std::cout << "Stop annoying user." }

   iterator iter_begin() { /* return begin iterator */ }
   iterator iter_end() { /* return end iterator */ }
};


auto begin(weird_container& c) {
   return c.iter_begin();
}

auto end(weird_container& c) {
   return c.iter_end();
}

However, you wouldn't and shouldn't do such a crazy thing as range-for would break if used with an object of weird_container, as per rules of range-for, the weird_container::begin() and weird_container::end() methods would be found before the stand-alone function variants.

This case therefore brings an argument not to use what you have proposed, as it would break one very useful feature of the language.

Case 2 - begin and end methods aren't defined at all

Another case is when you don't define the begin and end methods. This is a more common and applicable case, when you want to extend your type to be iteratable without modifying the class interface.

struct good_ol_type {
   ...
   some_container& get_data();
   ...
};

auto begin(good_ol_type& x) {
   return x.get_data().begin();
}

auto end(good_ol_type& x) {
   return x.get_data().end();
}

This would enable you to use some nifty features on good_ol_type (algorithms, range-for, etc) without actually modifying its interface! This is in line with Herb Sutter's recommendation of extending the functionality of types through non-member non-friend functions.

This is the good case, the one where you actually want to overload std:;begin and std::end.

Conclusion

As I haven't ever seen someone do something like that of the first case (except for my example), then you'd really want to use what you've proposed and overload std::begin and std::end wherever applicable.


I did not include here the case where you defined both begin and end methods, and begin and end functions that does different things than the methods. I believe such a situation is contrived, ill-formed and/or done by a programmer who haven't had much experience delving into the debugger or reading novel template errors.

Hilariahilario answered 13/9, 2013 at 7:52 Comment(2)
I get what you're trying to say but overloading is the wrong terminology here. You can't overload functions in the std namespace.Yoohoo
@Yoohoo Thanks for pointing that out. I'll be sure to clear that up. Thanks again.Hilariahilario
A
1

If your some_container is standard container, std:: prefix is needless

#include <iostream>
#include <vector>
#include <algorithm>
int main(){ 
       std::vector<int>v { 1, 7, 1, 3, 6, 7 };
       std::sort( begin(v), end(v) ); // here ADL search finds std::begin, std::end
}
Ankylostomiasis answered 13/9, 2013 at 7:32 Comment(2)
Even the std:: prefix on sort is needless. But all that holds IF it's a standard container. The question here is about the general case, where the container might or might not be a standard one. In particular, you can't assume that the container must be from ::std:: or :: (global namespace), it may come from anywhere.Lymphoblast
@Lymphoblast actually the std::sort is neccessary because std::vector::iterator could be a pointer or a type in a nested namespace, in which case ADL will not find std::sort.Yoohoo
D
0

the documentation of swap specifies that the idiom you refer to is common practice in the stl library

Many components of the standard library (within std) call swap in an unqualified manner to allow custom overloads for non-fundamental types to be called instead of this generic version: Custom overloads of swap declared in the same namespace as the type for which they are provided get selected through argument-dependent lookup over this generic version.

No such thing is present in the documentation for begin and end.

For this reason, you can definitely use the

using std::begin;
using std::end;
some_algorithm(begin(some_container), end(some_container));

calling convention, but you must be aware that this is a convention which doesn't apply to e.g. standard algorithms but to your code only.

Deuteron answered 13/9, 2013 at 7:17 Comment(4)
"cplusplus.com" is an assumptionLymphoblast
@msalters beg you pardon?!Deuteron
@StefanoFalasca the reference site you use is known to be full of errors. Prefer to use the latest draft Standard, or at the very least en.cpp.reference.comHerbalist
@Herbalist I assume you are right, and thank you very much for making me aware of this! didn't know.Deuteron

© 2022 - 2024 — McMap. All rights reserved.