Просмотр необработанного указателя в виде диапазона в цикле для цикла

Как заставить raw-указатель вести себя как диапазон для синтаксиса цикла 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

Мотивация:

Теперь vox populi указывает, что значение boost::optional (future std::optional) может рассматриваться как диапазон и поэтому используется в цикле диапазона for 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 << *dptr << std::endl;. Что хорошо, но я хотел достичь другого синтаксиса выше.

Попытки

Сначала я попытался сделать приведенную выше Optional версию begin и end для указателей, но я не смог. Поэтому я решил быть явным в типах и удалять все шаблоны:

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

Что происходит? Есть ли магия компилятора, которая запрещает работать с указателем. Я принимаю неверное предположение о синтаксисе Ranged-loop?

По иронии судьбы, в стандарте есть перегрузка для std::begin(T(&arr)[N]), и это очень близко к ней.


Примечание и вторая, хотя

Да, идея глупа, потому что, даже если это возможно, это будет очень запутанным:

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

будет перебирать только первый элемент. Более понятным и реалистичным обходным путем было бы сделать что-то вроде обходного пути, предложенного @Yakk:

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

Таким образом, очевидно, что мы выполняем итерацию только по одному элементу и этот элемент является необязательным.

Хорошо, хорошо, я вернусь к if(ptr) ... use *ptr.

Ответ 1

Потому что способ, основанный на диапазоне для работ (из § 6.5.5):

begin-expr и end-expr определяются следующим образом: - если _RangeT - тип массива, [..]
- если _RangeT - тип класса, [..]
- в противном случае begin-expr и end-expr равны begin(__range) и end(__range) соответственно, где beginи end просматриваются в соответствующих пространствах имен (3.4.2). [Примечание: обычный неквалифицированный поиск (3.4.1) не выполняется. -end note]

Каковы связанные пространства имен в этом случае? (§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}) { .. }

Ответ 2

Полезно полагать, что циклы for(:) реализуются путем вызова std::begin и std::end в контексте, активированном ADL ". Но это ложь.

Стандарт вместо этого в основном выполняет параллельную реализацию std::begin и std::end сам по себе. Это препятствует построению низкоуровневых языков в зависимости от его собственной библиотеки, что кажется хорошей идеей.

Единственный поиск для begin по языку - это поиск на основе ADL. Ваш указатель std::begin не будет найден, если вы не указатель на что-то в std. Компилятор std::begin( T(&)[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.

Обратите внимание, что rvalue optional для не ссылки ссылается на T const*, а rvalue optonal на T& возвращает a T*. Итерация по временному в контексте записи, вероятно, является ошибкой.