Viatorus
Viatorus

Reputation: 1903

std::begin - User-defined overloads not considered in type traits

Consider following type trait:

template<typename T, typename = void>
struct has_begin : std::false_type {};

template<typename T>
struct has_begin<T, std::void_t<
    decltype(std::begin(std::declval<std::add_lvalue_reference_t<T>>()))
  >> : std::true_type {};

Why does this trait does not consider my user-defined overload for std::begin?

namespace std {
void begin(foo&) {}
}

int main() {
  static_assert(has_begin<foo>::value); // Fails.

  foo f;
  std::begin(f); // Works.
}

Live example

Interesting observations:

  1. if I change the order of this type trait and my overload, it works
  2. if I use ADL in the type trait:

decltype(std::begin(std::add_lva... -> decltype(begin(std::add_lva...

it works if the free function begin is in the same namespace as foo:

void begin(foo) {
}

but fails for any class outside std:: depending on:

template<class C>
auto begin(C& c) -> decltype(c.begin());

because ADL lookup does not work with templates from other namespaces.


What could I do to support std::begin(foo&) in my type trait without changing the include order?

Otherwise I have to support both worlds - writing the type trait for std::begin and ADL begin()...

In my functions I already do something like this (suggested here):

auto get_begin() {
  using std::begin;
  return begin(object);
}

Upvotes: 4

Views: 185

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275405

You can solve this with a bit of indirection.

namespace adl {
  namespace std_adl {
    using std::begin; using std::end;
    template<class T>
    auto adl_begin( T&& t )
    ->decltype(begin(std::forward<T>(t)) )
      { return begin(std::forward<T>(t)); }
    template<class T>
    auto adl_end( T&& t )
    ->decltype(end(std::forward<T>(t)) )
      { return end(std::forward<T>(t)); }
  }
  template<class T>
  auto begin(T&& t)
  ->decltype(std_adl::adl_begin(std::forward<T>(t)))
    { return std_adl::adl_begin(std::forward<T>(t)); }
  template<class T>
  auto end(T&& t)
  ->decltype(std_adl::adl_end(std::forward<T>(t)))
    { return std_adl::adl_end(std::forward<T>(t)); }
}

now use adl::begin in place of std::begin when you want to enable ADL, both in your has_begin type trait and at point of use.

As for this:

namespace std {
  void begin(foo&) {
  }
}

that makes your program ill-formed no diagnostic required.

Upvotes: 3

Nicol Bolas
Nicol Bolas

Reputation: 473447

What could I do to support std::begin(foo&) in my type trait without changing the include order?

You don't; std::begin is not meant to be called directly for arbitrary ranges. If you want to access begin/end for a range type, you're supposed to use ADL, combined with using std::begin/end. That's just how the idiom works in C++.

It is illegal to overload methods in the std namespaces, and std::begin is no exception. You can create a template specializations of std-defined templates (based on user-created types), but this is not the proper way to use the C++ idiom.

In C++20, the std::ranges::begin function is meant to be called directly, and the way you specialize it for a type is through ADL or a member begin function. So just use the idiom, and everyone will be fine.

Upvotes: 6

Related Questions