【问题标题】:Viewing a raw pointer as a range in range-based for-loop将原始指针视为基于范围的 for 循环中的范围
【发布时间】:2015-03-30 07:03:00
【问题描述】:

对于 for-range 循环语法,如何使原始指针表现得像一个范围。

double five = 5;
double* dptr = &five;
for(int& d : dptr) std::cout << d << std::endl;// will not execute if the pointer is null

动机:

现在,boost::optional(未来的std::optional)值可以被视为一个范围,因此可以在 for range 循环http://faithandbrave.hateblo.jp/entry/2015/01/29/173613 中使用。

当我重写我自己的简化版本时:

namespace boost {
    template <class Optional>
    decltype(auto) begin(Optional& opt) noexcept{
        return opt?&*opt:nullptr;
    }

    template <class Optional>
    decltype(auto) end(Optional& opt) noexcept{
        return opt?std::next(&*opt):nullptr;
    }
}

用作

boost::optional<int> opt = 3;
for (int& x : opt) std::cout << x << std::endl;

在查看该代码时,我想它也可以泛化为原始(可为空)指针。

double five = 5;
double* dptr = &five;
for(int& d : dptr) std::cout << d << std::endl;

而不是通常的if(dptr) std::cout &lt;&lt; *dptr &lt;&lt; std::endl;。这很好,但我想实现上面的其他语法。

尝试

首先,我尝试使上述Optional 版本的beginend 用于指针,但我做不到。所以我决定明确类型并删除所有模板:

namespace std{ // excuse me, this for experimenting only, the namespace can be removed but the effect is the same.
    double* begin(double* opt){
        return opt?&*opt:nullptr;
    }
    double* end(double* opt){
        return opt?std::next(&*opt):nullptr;
    }
}

差不多了,它适用于

for(double* ptr = std::begin(dptr); ptr != std::end(dptr); ++ptr) 
    std::cout << *ptr << std::endl;

但它不适用于 假定等效的 for-range 循环:

for(double& d : dptr) std::cout << d << std::endl;

两个编译器告诉我:error: invalid range expression of type 'double *'; no viable 'begin' function available

发生了什么事?是否有一种编译器魔法禁止范围循环为指针工作。我是否对范围循环语法做出了错误的假设?

具有讽刺意味的是,在标准中,std::begin(T(&amp;arr)[N]) 有一个重载,这非常接近它。


请注意一下

是的,这个想法很愚蠢,因为即使可能,这也会非常令人困惑:

double* ptr = new double[10];
for(double& d : ptr){...}

只会遍历第一个元素。一个更清晰、更现实的解决方法是做类似@Yakk 提出的解决方法:

for(double& d : boost::make_optional_ref(ptr)){...}

通过这种方式,很明显我们只迭代了一个元素,并且该元素是可选的。

好的,好的,我回if(ptr) ... use *ptr

【问题讨论】:

  • ADL 永远无法找到您的 beginend,因此基于范围的 for 将不起作用。而且我不明白如何将optional 视为范围?!您应该能够通过使用这些指针构造一个boost::iterator_range 来让您的示例工作,但是以您的方式形成一个标量对象的 end 迭代器 很可能是未定义的行为。跨度>
  • @Praetorian 实际上是合法的;出于指针算术目的,不是数组元素的东西被认为是大小为 1 的数组。不过,将重载添加到 namespace std 绝对是 UB,并且为此滥用基于范围的 for 是相当愚蠢的。跨度>
  • @T.C.啊,好的,谢谢你的澄清,我不确定那部分。
  • 奇怪的是 std::begin(T(&amp;arr)[N]) 不用于基于范围的 for 循环。核心语言直接处理数组:“如果_RangeT是数组类型,begin-exprend-expr分别是__range__range + __bound,其中__bound 是数组绑定。"

标签: c++ pointers c++11 for-loop boost-optional


【解决方案1】:

因为基于范围的工作方式是(来自 §6.5.4):

begin-exprend-expr确定如下
— 如果_RangeT 是数组类型,[..]
— 如果_RangeT 是类类型,[..]
— 否则,begin-exprend-expr 分别为 begin(__range)end(__range),其中 beginend 在关联的命名空间 (3.4.2) 中查找。 [注:普通不合格查找(3.4.1) 不执行。 ——尾注]

在这种情况下,关联的命名空间是什么? (§3.4.2/2,强调我的):

命名空间和类的集合通过以下方式确定:
(2.1) — 如果T 是基本类型,则其关联的命名空间和类集都是空的

因此,没有地方可以放置您的double* begin(double*),这样它就会被基于范围的for 语句调用。

您想要做的解决方法是制作一个简单的包装器:

template <typename T> 
struct PtrWrapper {
    T* p;
    T* begin() const { return p; }
    T* end() const { return p ? p+1 : nullptr; }
};

for (double& d : PtrWrapper<double>{dptr}) { .. }

【讨论】:

  • 或者,非标准版本:虽然认为std::beginstd::end 在ADL 上下文中被for(:) 使用循环会让你了解大部分真相,但标准几乎是重新实现没有提到它们或使用它们。这主要是因为低级语言结构不依赖于库。
  • @T.C.大声笑,是的,我在没有注意的情况下复制了 OP 所做的事情
  • 不幸的是,问题是原则上可以做到这一点,但不能将函数放在任何命名空间中。最后是“编译器魔法”(或缺乏)。
  • @alfC 实际上不,这不是“编译器魔法”。这是“定义明确的标准语言行为”。
  • 我的意思是他们可以将T* 设为一个特殊情况,就像他们为T(&amp;)[N] 所做的那样。也许他们没有这样做,因为这会使double* dptr = new double[10]; for(double&amp; d : dptr){...}; 感到困惑,因为for 只会做第一个元素。
【解决方案2】:

认为for(:) 循环是通过“在ADL 激活的上下文中调用std::beginstd::end”来实现的,这是一个有用的谎言。但那是谎言。

该标准本身基本上是并行实现std::beginstd::end。这可以防止语言的低级结构依赖于自己的库,这似乎是个好主意。

语言对begin 的唯一查找是基于 ADL 的查找。将找不到您的指针 std::begin,除非您是指向 std 中某些内容的指针。 std::begin( T(&amp;)[N} ) 不是由编译器以这种方式找到的,而是由语言硬编码的迭代。

namespace boost {
  template<class T>
  T* begin( optional<T>&o ) {
    return o?std::addressof(*o):nullptr;
  }
  template<class T>
  T* begin( optional<T&>&&o ) {
    return o?std::addressof(*o):nullptr;
  }
  template<class T>
  T const* begin( optional<T> const&o ) {
    return o?std::addressof(*o):nullptr;
  }
  template<class T>
  T* end( optional<T>&o ) {
    return o?std::next(begin(o)):nullptr;
  }
  template<class T>
  T* end( optional<T&>&&o ) {
    return o?std::next(begin(o)):nullptr;
  }
  template<class T>
  T const* end( optional<T> const&o ) {
    return o?std::next(begin(o)):nullptr;
  }
  template<class T>
  boost::optional<T&> as_optional( T* t ) {
    if (t) return *t;
    return {};
  }
}

现在你可以:

void foo(double * d) {
  for(double& x : boost::as_optional(d)) {
    std::cout << x << "\n";
}

无需重复输入double

请注意,非引用的右值 optional 返回 T const*,而 T&amp; 的右值 optonal 返回 T*。在写作上下文中迭代临时可能是一个错误。

【讨论】:

  • 我做了begin(Optional&amp;) 正是为了避免所有的过载。我应该做Optional&amp;&amp;
  • Intead of as_optional(在您的帖子末尾)我可以使用boost::make_optional(dptr, *dptr)(似乎可行,但我不确定评估顺序)。这让我觉得 make_optional 可能有一个指针过载 T* 来做到这一点。
  • @alfC 除了你的作品不是optionals。 boost::mutex 看起来对您的代码是可迭代的。
  • 是的,但它就像一个可选的。那时我意识到如果我可以将它放在正确的命名空间中,它也可以用于指针......但它没有,因为没有正确的命名空间(正如另一个答案指出的那样)。我不熟悉mutex,但它不会在std::nextoperator* 之后软失败吗?我正在阅读en.cppreference.com/w/cpp/thread/mutexboost.org/doc/libs/1_31_0/libs/thread/doc/mutex.html
  • @alfC 我是说你至少需要一个 SFINAE 测试。您不想begin 的正文中失败,而是希望通过未能匹配覆盖而失败。你的begin 覆盖太贪婪,太贪婪是粗鲁的,可能会导致错误。 boost:: 中的每个类都将传递给您的begin,即使它不合适。
【解决方案3】:

TL;DR

此构造可用于范围 for 循环:

std::views::counted(raw_ptr, !!raw_ptr)

详情

C++20 使用 ranges 库提供了多种创建临时可迭代对象的方法。对于example

#include <ranges>
#include <iostream>
 
int main()
{
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    int *raw_ptr = a;
    
    for(int i : std::views::counted(raw_ptr, 10))
        std::cout << i << ' ';
    std::cout << '\n';
    
    for(int i : std::views::counted(raw_ptr, 1))
        std::cout << i << ' ';
    std::cout << '\n';
    
    std::cout << "empty for null pointer pointer\n";
    raw_ptr = nullptr;
    for(int i : std::views::counted(raw_ptr, 0))
        std::cout << i << ' ';
    std::cout << '\n';
    
    std::cout << "Exit\n";
}

打印

1 2 3 4 5 6 7 8 9 10 
1

empty for null pointer

Exit

std::views::subrange 也可以与 (start, end] 指针一起使用。查看library 了解更多信息。

【讨论】:

  • @Sopel,好点子,也许解决方法是std::views::counted(raw_ptr, !!raw_ptr)。或 Ranges 可以有一个 std::views::optional(raw_ptr) 来执行此操作(至少对于可为空的、布尔可比较的、迭代器)。 godbolt.org/z/9PWr5T
  • @Sopel 你错了coliru.stacked-crooked.com/a/67e130f39cc46568。与任何“计数范围”一样,您必须知道元素的数量,在这种情况下为 0;之后nullptr 没有特别的问题
  • @alfC 不是一个好点:P 按照这个逻辑,没有counted_range 有效。如果您给出集合的计数 > 大小,您将访问其中的内存,因此始终由程序员提供正确的计数
  • @LorahAttkins,根据我的问题,正确的计数将由指针本身给出。如果指针为空,则计数为零,否则为一。所以,views::counted 可以通过 std::views::counted(raw_ptr, raw_ptr?1:0) 来使用。
  • @alfC 我认为这个答案概括了迭代原始指针的概念。指针本身不包含有关元素数量的信息。是的,如果我们谈论的是单个元素,那么您说的是真的,但通常它应该是std::views::counted(raw_ptr, raw_ptr ? n : 0),程序员应该知道n。在任何情况下raw_ptr 成为NULL 与@Sopel 所说的相比不会引起任何问题;重要的是count
猜你喜欢
  • 1970-01-01
  • 2016-10-31
  • 1970-01-01
  • 2013-01-04
  • 1970-01-01
  • 1970-01-01
  • 2014-12-06
  • 1970-01-01
  • 2018-12-28
相关资源
最近更新 更多