Я сделал это input iterator
template <typename T>
struct input_iter: base_it<T> {
private:
typename base_it<T>::pointer _ptr;
public:
constexpr input_iter<T>() = default;
constexpr explicit input_iter<T>(typename base_it<T>::pointer ptr = nullptr)
: _ptr { ptr } {}
constexpr ~input_iter<T>() = default;
constexpr input_iter<T>(const input_iter<T>& other) = default;
constexpr input_iter<T>(input_iter<T>&& other) noexcept = default;
[[nodiscard]]
constexpr auto operator=(typename base_it<T>::pointer ptr) -> input_iter<T>& {
_ptr = ptr; return *this;
}
constexpr auto operator=(const input_iter<T>&) -> input_iter<T>& = default;
constexpr auto operator=(input_iter<T>&&) noexcept -> input_iter<T>& = default;
[[nodiscard]]
constexpr auto operator*() const noexcept -> const typename base_it<T>::reference {
return *_ptr;
}
[[nodiscard]]
constexpr auto operator->() const noexcept -> const typename base_it<T>::pointer {
return _ptr;
}
constexpr auto operator++() noexcept -> input_iter& {
++this-> _ptr;
return *this;
}
constexpr void operator++(int) noexcept {
++(*this);
}
[[nodiscard]]
constexpr friend auto operator==(const input_iter& lhs, const input_iter& rhs) noexcept -> bool {
return lhs._ptr == rhs._ptr;
}
[[nodiscard]]
constexpr friend auto operator!=(const input_iter& lhs, const input_iter& rhs) noexcept -> bool {
return not (lhs == rhs);
}
};
Где вы должны предположить (на данный момент), что base_iter
есть std::iterator
.
Ну, одно обязательное требование для input_iterator
— это должно быть indirectly_readable
. Поскольку мой input_iter
не принадлежит какой-либо конкретной структуре данных и находится в отдельном модуле, потому что я хочу сделать его доступным для контейнеров или диапазонов, элементы которых хранятся в смежных областях памяти (но это история для другого поста SO) Я хотел бы ограничить операцию записи вещей в базовый контейнер или диапазон. Итак, моя идея заключается в следующем:
template <typename T>
using base_it = std::iterator<std::input_iterator_tag, const T>;
Обратите внимание на const T, а не T в псевдониме
Поэтому всякий раз, когда я пытаюсь написать стамент такого рода:
collections::Array arr = collections::Array<int, 5>{1, 2, 3, 4, 5};
auto it_begin = arr.begin();
*it_begin = 7;
Я получаю ошибку, которую хочу, во время компиляции!
.\zero\tests\iterators\legacy\legacy_iterator_tests.cpp:47:28: error: cannot assign to return value because function
'operator*' returns a const value
*it_begin = 7;
Отлично. Но...
static_assert
и проверить, что это выражение не компилируется? Я видел сложные вещи, происходящие с лямбда-выражениями в блоке else в if constexpr
, и еще кое-что с использованием SFINAE
, но я не могу написать это самостоятельно. Кто-нибудь может объяснить, как это написать?Код ниже - это то, что я пробовал до сих пор, но безуспешно. Воспринимайте это как метаидею.
if constexpr (some_cond)
static_assert(
*it_begin = 7,
"Wait... this is compiling! This shouldn't happen, since an input iterator musn't be able to performn write operations");
! requires(/*..*/){*it_begin = 7;}
?
@TedLygmo не может ли const T*
продвинуться на произвольное количество несмежных шагов? Например, как random_access_iterators
для std::list? Кстати, я просто хочу знать, как я могу сделать тест времени компиляции, который «проходит» всякий раз, когда фрагмент кода не компилируется (например, *it_begin = 7
для моего входного итератора)
@ Jarod42, спасибо, но я не могу полностью понять твой фрагмент. Не могли бы вы быть более явным? (нуб С++ здесь)
@AlexVergara Да, const T*
- это итератор с произвольным доступом, которым является большинство итераторов, работающих с непрерывными диапазонами. std::list
не имеет итераторов произвольного доступа. Структура памяти данных std::list
не является непрерывной, потому что было бы невозможно выполнить другие требования для std::list
, если бы это было так.
@TedLyngmo извините, я всегда путаю std::list
с std::array
, но да, я просто хотел указать, что T*
это random_access
. Итак, я понимаю, что вы бы сказали, что T*
может быть непрерывным, а не то, что оно также может действовать как непрерывное, потому что произвольный доступ подразумевает непрерывность. Извините за путаницу
@AlexVergara Нет проблем! Однако итератор произвольного доступа не обязательно означает, что доступные данные являются непрерывными. Например, можно хранить метаданные между данными, доступными пользователю (пример), и это все равно будет итератор с произвольным доступом.
@TedLyngmo, спасибо, что поделились. Хороший пример
@AlexVergara Не за что. Я только что заметил, что iterator operator++(int)
в моем примере глючит (копирует p
вместо *this
), но вы хоть картинку поняли :-)
Что касается статического утверждения: почему бы просто не использовать концепцию, если вы используете С++ 20?
{*d = from}
— это выражение, которое должно компилироваться во время
-> std::convertible_to<decltype(*d)>;
часть является необязательным типом этого выражения. Его можно сделать так, чтобы он соответствовал точному типу с помощью same_as
, или его можно просто удалить, если тип не имеет значения.
#include <concepts>
#include <vector> //just for demonstration purposes
template<typename Dereferencable, typename From>
concept DereferencedAssignable = requires(Dereferencable d, From from)
{
{*d = from} -> std::convertible_to<decltype(*d)>; //or std::same_as, depending on one's needs
};
static_assert(DereferencedAssignable<int*, int>);
static_assert(not DereferencedAssignable<const int*, int>);
static_assert(DereferencedAssignable<std::vector<double>::iterator, double>);
static_assert(not DereferencedAssignable<std::vector<double>::const_iterator, double>);
https://godbolt.org/z/8s44Gb3ME
Что касается идиоматичности: Я бы предпочел вернуть копию предыдущего значения в операторе приращения постфикса, но здесь это могло быть лишь незначительной оплошностью.
Concepts был моей первой попыткой, но я не смог понять правильный синтаксис! В основном потому, что я пытался сделать концепцию достаточно общей, чтобы принять любое выражение.
@AlexVergara Я видел довольно общую вещь «проверить, компилируется ли это», созданную с использованием комбинированных макросов, лямбда-выражений и void_t. Это можно сделать, конечно. Стоит ли оно того? Это зависит от очень общего кода, может быть. Не стесняйтесь открывать другую тему для этого, если вам это нужно.
одно сомнение. Зачем возвращать копию предыдущего значения в операторе приращения постфикса?
Предсказуемость API, условности и многое другое. Обычно язык допускает такое использование: int x = 3; int y = ++x; // y is 4
, int x = 3; int y = x++; // y is 3
.
ах, конечно. Хорошая точка зрения. Спасибо, что заметили это!
Пожалуйста. Кстати, я видел, что ваш вопрос удален. В любом случае, если вы хотите увидеть обман, вот он в версии C++17: godbolt.org/z/1GnrrGsde Я еще не переработал его в концепции, я могу опубликовать его здесь, когда сделаю ;)
Вау, они невероятные. Большое спасибо за это! К сожалению, вопрос закрыт :( Я хотел бы открыть его заново, чтобы вы могли заслужить ту заслугу, которая у вас есть за такие невероятные ответы. Действительно, спасибо, чувак
@AlexVergara не упоминайте об этом, некоторые люди предпочитают кроссворды, у меня есть шаблоны C++ ;-) Рад, что мог быть полезен, ура.
Я где-то заблудился, но что делает ваш итератор, чего не делает
const T*
?const T*
работает как интератор в непрерывной памяти, и вы получитеconst T&
, если разыменуете его.