3

How can I make a raw pointer behave like a range, for a for-range loop syntax.

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

Motivation:

It is now vox populi that an boost::optional (future std::optional) value can be viewed as a range and therefore used in a for range loop http://faithandbrave.hateblo.jp/entry/2015/01/29/173613.

When I rewrote my own simplified version of it:

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

Used as

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

While looking that code I imagined that it could be generalized to raw (nullable) pointers as well.

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

instead of the usual if(dptr) std::cout << *dptr << std::endl;. Which is fine but I wanted to achieve the other syntax above.

Attempts

First I tried to make the above Optional version of begin and end work for pointers but I couldn't. So I decided to be explicit in the types and remove all templates:

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

Almost there, it works for

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

But it doesn't work for the supposedly equivalent for-range loop:

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

Two compilers tell me: error: invalid range expression of type 'double *'; no viable 'begin' function available

What is going on? Is there a compiler magic that forbids the ranged-loop to to work for pointers. Am I making a wrong assumption about the ranged-loop syntax?

Ironically, in the standard there is an overload for std::begin(T(&arr)[N]) and this is very close to it.


Note and a second though

Yes, the idea is silly because, even if possible this would be very confusing:

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

would iterate over the first element only. A more clear and also realistic workaround would be to do something like workaround proposed by @Yakk:

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

In this way it is clear that we are iterating over one element only and that that element is optional.

Ok, ok, I will go back to if(ptr) ... use *ptr.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • Your `begin` and `end` can never be found by ADL, so the range based `for` is not going to work. And I don't understand how one can think of an `optional` as a *range*?! You should be able to get your example to work by constructing a `boost::iterator_range` with those pointers, but forming an *`end` iterator* to a scalar object the way you are is most likely undefined behavior. – Praetorian Jan 30 '15 at 18:27
  • @Praetorian Actually it's legal; for pointer arithmetic purposes something that's not an array element is considered to be in an array of size 1. Adding overloads to `namespace std` is definitely UB, though, and abusing the range-based `for` for this is rather silly. – T.C. Jan 30 '15 at 18:31
  • @T.C. Ah, ok, thanks for clarifying, I wasn't sure of that part. – Praetorian Jan 30 '15 at 18:35
  • 5
    The weird thing is that `std::begin(T(&arr)[N])` isn't used for range-based for loops. The core language handles arrays directly: "if `_RangeT` is an array type, *begin-expr* and *end-expr* are `__range` and `__range + __bound`, respectively, where `__bound` is the array bound." – Casey Jan 30 '15 at 18:39
  • Related: http://faithandbrave.hateblo.jp/entry/2015/01/29/173613 – alfC Aug 03 '16 at 01:24

3 Answers3

7

Because the way that range-based for works is (from §6.5.4):

begin-expr and end-expr are determined as follows
— if _RangeT is an array type, [..]
— if _RangeT is a class type, [..]
— otherwise, begin-expr and end-expr are begin(__range) and end(__range), respectively, where begin and end are looked up in the associated namespaces (3.4.2). [ Note: Ordinary unqualified lookup (3.4.1) is not performed. —end note ]

What are the associated namespaces in this case? (§3.4.2/2, emphasis mine):

The sets of namespaces and classes are determined in the following way:
(2.1) — If T is a fundamental type, its associated sets of namespaces and classes are both empty.

Thus, there is no place to put your double* begin(double*) such that it will be called by the range-based for statement.

A workaround for what you want to do is just make a simple wrapper:

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}) { .. }
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 3
    Or, the non-standardese version: while thinking that `std::begin` and `std::end` in an ADL context are used by `for(:)` loops will get you most of the truth, the standard instead almost reimplements that without referring to them or using them. This is mostly so low level language constructs do not depend on the library. – Yakk - Adam Nevraumont Jan 30 '15 at 18:35
  • @T.C. lol, yeah, I copied what OP sort of did without paying attention – Barry Jan 30 '15 at 18:40
  • What unfortunate, the problem is that in principle one can do this, but cannot put the functions in any namespace. It was "compiler magic" (or the lack of) at the end. – alfC Jan 30 '15 at 19:10
  • @alfC Actually no, it's not "compiler magic." It's "well-defined standard language behavior." – Barry Jan 30 '15 at 19:14
  • What I mean is that they could have made `T*` a special case, like they did for `T(&)[N]`. Maybe they didn't because this would have been confusing `double* dptr = new double[10]; for(double& d : dptr){...}; ` since the `for` will do the first element only. – alfC Jan 30 '15 at 19:53
3

It is a useful lie to think that for(:) loops are implemented by "calling std::begin and std::end in a ADL-activated context". But that is a lie.

The standard instead basically does a parallel implementation of the std::begin and std::end in itself. This prevents the language's low level constructs from depending on its own library, which seems like a good idea.

The only lookup for begin by the language is the ADL-based lookup. Your pointer's std::begin won't be found, unless you are a pointer to something in std. The std::begin( T(&)[N} ) isn't found this way by the compiler, but instead that iteration is hard-coded by the language.

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

now you can:

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

without having to repeat the type double.

Note that an rvalue optional to a non-reference returns a T const*, while an rvalue optonal to a T& returns a T*. Iterating over a temporary in a writing context is probably an error.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I did the `begin(Optional&)` precisely to avoid all the overloads. I should have done `Optional&&`. – alfC Jan 30 '15 at 19:06
  • Intead of `as_optional` (towards the end of your post) I could use `boost::make_optional(dptr, *dptr)` (seems to work, but I am not sure about the evaluation order). This makes me think that `make_optional` could have an overload of pointers `T*` to do this. – alfC Jan 30 '15 at 19:09
  • @alfC except your works on things that are not `optional`s. A `boost::mutex` looks iterable to your code. – Yakk - Adam Nevraumont Jan 30 '15 at 19:19
  • Yes, but it quacks like an optional. That's when I realized it could work for pointers as well if I could put it in the right namesapce... and it didn't because there is no right namespace for this (as the other answer pointed out). I am no familiar with `mutex` but wouldn't it soft fail at `std::next` or after `operator*`? I am just reading http://en.cppreference.com/w/cpp/thread/mutex and http://www.boost.org/doc/libs/1_31_0/libs/thread/doc/mutex.html – alfC Jan 30 '15 at 19:22
  • @alfC I'm saying you need at least a SFINAE test. You don't want to fail **in the body of your `begin`**, you want to fail by failing to match the override. Your `begin` override is too greedy, and being too greedy is rude and can cause bugs. **every class in `boost::`** will be passed to your `begin`, even if it is not appropriate. – Yakk - Adam Nevraumont Jan 30 '15 at 19:26
  • Wouldn't the `decltype(auto)` work as SFINAE? at least `auto ... -> decltype(opt?&*std::forward(opt):nullptr)` would right? – alfC Jan 30 '15 at 19:28
  • @alfC no `decltype(auto)` does not do SFINAE. The other might, but now anything that overrides unary `*` (say a regex? that uses it for kleen star?) and `operator bool` (does regex?) in namespace `boost` now appears to be iterable. Don't pollute huge namespaces with overloads that glom on to everything. To make that safe, you'd have to understand every `boost` type and every type that **will** be added to `boost` while your modification is in there, and that is simply not possible. – Yakk - Adam Nevraumont Jan 30 '15 at 19:29
  • Thanks, I think it is clear now, also ignore my comment about `boost::make_optional(dptr, *dptr)`. That would create a copy and that is unacceptable. – alfC Jan 30 '15 at 20:04
  • @alfc and can create an actual reference from a null pointer, which is illegal. – Yakk - Adam Nevraumont Jan 30 '15 at 20:30
  • Yes. But it seemed to work and that confused me. Your `as_optional` is correct I think. – alfC Jan 30 '15 at 20:33
1

TL;DR

This construct can be used in a range for loop:

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

Details

C++20 offers a plethora of ways to create ad-hoc iterables, using the ranges library. For 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";
}

Prints

1 2 3 4 5 6 7 8 9 10 
1

empty for null pointer

Exit

Similarly std::views::subrange could be used with the (start, end] pointers. Check the library for more info.

Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • @Sopel, good point, maybe a workaround could be `std::views::counted(raw_ptr, !!raw_ptr)`. or Ranges could have an std::views::optional(raw_ptr) that does this (at least for a nullable, boolean-comparable, iterator). http://godbolt.org/z/9PWr5T – alfC Oct 05 '20 at 00:18
  • 1
    @Sopel You are wrong http://coliru.stacked-crooked.com/a/67e130f39cc46568. As with any "counted range" you have to know the number of elements, which in this case is 0; after that `nullptr` creates no particular problem – Nikos Athanasiou Oct 10 '20 at 11:33
  • 2
    @alfC Not a good point :P By this logic there is no `counted_range` that works. If you give a count > size of the collection you'll access memory out of it, so it always fall on the programmer to provide a correct count – Lorah Attkins Oct 10 '20 at 11:41
  • @LorahAttkins, according to my question, the correct count would be given by the pointer itself. If the pointer is null the count is zero, otherwise one. So, `views::counted` could be used by doing `std::views::counted(raw_ptr, raw_ptr?1:0)`. – alfC Oct 10 '20 at 22:00
  • @alfC I think this answer generalizes on the concept of iterating raw pointers. The pointer itself does not contain information about the number of elements. Yes, if we're talking about a single element what you say is true, but generally it should be `std::views::counted(raw_ptr, raw_ptr ? n : 0)` and the programmer should know `n`. In any case `raw_ptr` being `NULL` doesn't cause any problems in contrast to what @Sopel says; it is the `count` that matters. – Lorah Attkins Oct 11 '20 at 12:37
  • @LorahAttkins, the original question was to replace code like `if(p) ...use *p;` with a range-for loop without explicit pointer dereference `for(auto& e: ???) ...use e;`. The count is either 0 or 1 and the arithmetic (and even proper iteration) is irrelevant. What I can get indirently from this answer is that `??? -> std::views::counted(p, p?1:0)` could have been a perfectly acceptable answer but as written it missed the point. There is no need to generalize to other counts. If anything, a generalization could be to any nullable pointer-like type (for example `T*`, `std::unique_ptr`, etc). – alfC Oct 12 '20 at 08:23
  • @alfC It's a pitty cause this is exactly contained in the answer, I didn't know it was trouble to extract. Nikos Athasaniou surely you can deduce from the comments what update is needed no? – Lorah Attkins Oct 12 '20 at 11:30