std::begin - 类型特征中不考虑用户定义的重载

4

考虑以下类型特征:

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 {};

为什么这个特性没有考虑我的用户定义重载 std::begin
namespace std {
void begin(foo&) {}
}

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

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

实时例子

有趣的观察结果:

  1. 如果我改变这个类型特征和我的重载的顺序,它就能工作
  2. 如果我在类型特征中使用ADL:

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

如果自由函数 beginfoo 在相同的命名空间中,则它可以工作。

void begin(foo) {
}

但是对于除了std::之外的任何类,取决于以下因素,此方法将失败:

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

由于 ADL 查找无法与其他名称空间中的模板配合使用,因此我需要做些什么才能在不改变包含顺序的情况下支持 std::begin(foo&) 呢?


否则,我就必须同时支持 std::begin 和 ADL begin() 这两个世界……

在我的函数中,我已经像这样做了一些事情(参考这里):

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

1
@uneven_mark 你确实很棒。虽然有例外情况,但有些甚至是被鼓励的。 - Ted Lyngmo
1
@uneven_mark 我对标准的了解很浅。如果您能使其正常工作,std::begin听起来是可以干扰的 - 但我不确定。 - Ted Lyngmo
1
@TedLyngmo 我现在参考的是cppreference。我还没有找到相关的标准部分。 - walnut
1
@uneven_mark 这是一个非常好的维基百科。下一个标准提案也非常有用:C++下一草案 - Ted Lyngmo
1
@uneven_mark 给你。我不是一个拘泥于规则的人,但我非常尊重cppref并倾向于顺其自然。 - Ted Lyngmo
显示剩余7条评论
2个回答

6
如果你想在不改变include顺序的情况下支持std::begin(foo&),那么你不能这样做。std::begin并不是为了任意范围而直接调用的。如果你想访问一个范围类型的begin/end,你应该使用ADL,再加上using std::begin/end。这就是C++中的惯用法。
在std命名空间中重载方法是非法的,std::begin也不例外。你可以创建基于用户创建的类型的std定义模板的特化,但这不是使用C++惯用法的正确方式。
在C++20中,std::ranges::begin函数是可以直接调用的,你可以通过ADL或成员begin函数对其进行类型特化。所以只需使用惯用法,一切都会好的。

抱歉打扰了,但有人澄清事情确实很好。我肯定会在C++20成为现实时回来的。谢谢! - Ted Lyngmo
谢谢。对我来说(也许对许多人来说),cppreference上的描述并不是很清楚。没有明确说明std::begin不应该直接调用。此外,对我来说,重载在std命名空间中是隐含的。感谢您提到std::ranges::begin,但这对我还不起作用:https://gcc.godbolt.org/z/tcv9KW - Viatorus

3

通过一些间接的方式,您可以解决这个问题。

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

现在在需要启用ADL的情况下,请使用adl::begin代替std::begin,无论是在您的has_begin类型特征中还是在使用点上。关于这一点:
namespace std {
  void begin(foo&) {
  }
}

使您的程序不完整,无需诊断。

谢谢。解决问题的好主意。 :) 我不知道 std::begin 是不合法的,因为 cppreference 对我来说不太清楚。 - Viatorus
@viat 不要在 std 中添加任何东西,除非是在特定情况下的一些专门化实现(而且永远不要重载)。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接