I am writing my own vector.h for fun (please don't judge). I am trying to remain const correct so I have implemented both a const .begin() function and non const .begin() functions for my Vector::iterator class. My CMakeLists.txt file is requiring C++23
In my UnitTest I am unfortunately able to declare a const vector then declare a non const iterator and successfully change the value of the const Vector using the iterator. This is obviously not the point of a const Vector.
Please If you have time look through my implementation to see what I am overlooking. Full code found on my github here. Snippets below:
TEST(testConstBegin) {
const Vector<int> c_myVec = {0,1,2,4};
Vector<int>::iterator it = c_myVec.begin(); // NOTE not const.
// c_myVec[0] = 4; // fails as expected
*it = 4; // should fail but does not. THIS IS WHY I WROTE THIS QUESTION.
CHECK_EQUAL(c_myVec[0], 0); // This line should never run.
}
template <typename T>
typename Vector<T>::iterator::iterator_reference
Vector<T>::iterator::operator=(
typename Vector<T>::iterator::const_iterator_reference other) {
if (this != &other)
iter_ = other.iter_;
return *this;
}
template <typename T>
typename Vector<T>::const_iterator
Vector<T>::cBegin() const {
return typename Vector<T>::iterator::const_iterator(array_);
}
// issue #001
template <typename T>
typename Vector<T>::const_iterator
Vector<T>::begin() const {
return typename Vector<T>::iterator::const_iterator(array_);
}
template <typename T>
typename Vector<T>::iterator
Vector<T>::begin() {
return iterator(array_);
}
// const operator*
template <typename T>
typename Vector<T>::iterator::const_reference
Vector<T>::iterator::operator*() const {
return *iter_;
}
// operator*
template <typename T>
typename Vector<T>::iterator::reference
Vector<T>::iterator::operator*() {
return *iter_;
}
/**
* Returns a read-only (constant) iterator that points to the
* first element in the %vector. Iteration is done in ordinary
* element order.
*/
const_iterator
begin() const _GLIBCXX_NOEXCEPT
{ return const_iterator(this->_M_impl._M_start); }
testVectorFillCtor.cpp:32:50: error: conversion from ‘__normal_iterator<const int*,[...]>’ to non-scalar type ‘__normal_iterator<int*,[...]>’ requested
32 | std::vector<int>::iterator it = c_myVec.begin();
| ~~~~~~~~~~~~~^~
Implementing a container is not easy, but if you do, I absolutely respect that your goal is to be const correct. In fact, don't do this kind of thing if you are not going to do it 100% "const-correct".
The original defect I see in your code is this one here,
https://github.com/PIesPnuema/stl_implementation_practice/blob/main/include/vector.h#L38
typedef const iterator const_iterator;
const_iterator
needs to be implemented as a different class than iterator
, so it can return const
references when dereferenced.
In any case, a const_iterator
has nothing to do with the iterator being const
.
Think about it, can a const_itertor
be mutated (e.g., by increment)? Probably yes, so it is definitely not the same as iterator const
.
This is the same difference as between double const*
and double* const
.
(This is the kind of confusion one doesn't have by using EastConst).
In other words, const_iterator
and iterator
are basically independent types.
For the case of a vector, they can be defined simply as double const*
and double*
respectively; but that will hide the general complexity of implementing iterators.
So, I will assume that we want iterators to be classes.
I have implemented vector-like containers; unfortunately, there is no other way.
You can save some code duplication by having a single common basic_iterator<Ref>
base parameterized in T&
and T const&
, but that is a different story.
This is the smallest code I came up with to illustrate this point:
#include<cassert>
class Vec {
double value_ = 5;
public:
struct iterator {
double* ptr_;
auto operator*() const -> double& {return *ptr_;}
};
struct const_iterator {
double const* ptr_;
// ... perhaps allow implicit conversion from iterator to const_iterator and other shenanigans here
auto operator*() const -> double const& {return *ptr_;}
};
auto begin() -> iterator {return {&value_};}
auto begin() const -> const_iterator {return {&value_};}
};
int main() {
Vec const v;
assert( *v.begin() == 5 );
// *v.begin() = 6; // double const& is not assignable, compilation error
}
https://godbolt.org/z/vGT8j3WrG
For a complete example, look at my container array library, follow the white rabbit from this line, https://gitlab.com/correaa/boost-multi/-/blob/master/include/multi/array_ref.hpp#L1267
To avoid some code duplication, you can use some template tricks:
class Vec {
double value_ = 5;
template<template<typename> class Modifier>
struct iterator_base {
Modifier<double>::type* ptr_;
auto operator*() const -> auto& {return *ptr_;}
};
public:
using iterator = iterator_base<std::type_identity>;
using const_iterator = iterator_base<std::add_const >;
auto begin() -> iterator {return {&value_};}
auto begin() const -> const_iterator {return {&value_};}
};
https://godbolt.org/z/81GK69G7W
If you want to completely eliminate code duplication and automatically convert from iterator
to cons_iterator
, you can do this, although it is unclear if it is a real gain.
class Vec {
std::unique_ptr<double> base_ = std::make_unique<double>(5.0);
template<template<typename> class ConstQ>
class iterator_base {
ConstQ<double>::type* ptr_;
friend class iterator_base<std::add_const>;
public:
explicit iterator_base(ConstQ<double>::type* ptr) : ptr_{ptr} {}
iterator_base(iterator_base<std::type_identity> const& other) : ptr_{other.ptr_} {}
auto operator*() const -> auto& {return *ptr_;}
};
public:
using iterator = iterator_base<std::type_identity>;
using const_iterator = iterator_base<std::add_const >;
private:
auto begin_aux() const {return iterator(base_.get());}
public:
auto begin() -> iterator {return begin_aux();}
auto begin() const -> const_iterator {return begin_aux();}
};
https://godbolt.org/z/PEqqdTefe
One can eliminate the spurious base class and the strange constructor by using the questionable technique of inheritance-for-extension; one has to be careful later when implementing mutation members on the iterators classes. Note the constness doesn't escape from the design if one is careful in the implementation:
class const_iterator {
const_iterator(double* ptr) : ptr_{ptr} {}
double* ptr_;
friend class Vec;
public:
auto operator*() const -> double const& {return *ptr_;}
};
struct iterator : const_iterator {
auto operator*() const -> double & {return *ptr_;}
};