The top answer, using ranges or spans, is a great solution, if you can use C++20 or later (or a library such as the GSL). If not, here are some other approaches.
Unsafe Cast
#include <vector>
class Data
{
public:
const std::vector<const int>& getPrimaryData() const
{
return *reinterpret_cast<const std::vector<const int>*>(&primaryData);
}
const std::vector<const int* const>& getIndex()
{
return *reinterpret_cast<const std::vector<const int* const>*>(&index);
}
private:
std::vector<int> primaryData;
std::vector<int*> index;
};
This is living dangerously. It is undefined behavior. At minimum, you cannot count on it being portable. Nothing prevents an implementation from creating different template overloads for const std::vector<int>
and const std::vector<const int>
that would break your program. For example, a library might add some extra private
data member to a vector
of non-const
elements that it doesn’t for a vector
of const
elements (which are discouraged anyway).
While I haven’t tested this extensively, it appears to work in GCC, Clang, ICX, ICC and MSVC.
Smart Array Pointers
The array specialization of the smart pointers allows casting from std::shared_ptr<T[]>
to std::shared_ptr<const T[]>
or std::weak_ptr<const T[]>
. You might be able to use std::shared_ptr
as an alternative to std::vector
and std::weak_ptr
as an alternative to a view of the vector
.
#include <memory>
class Data
{
public:
std::weak_ptr<const int[]> getPrimaryData() const
{
return primaryData;
}
std::weak_ptr<const int* const[]> getIndex()
{
return index;
}
private:
std::shared_ptr<int[]> primaryData;
std::shared_ptr<int*[]> index;
};
Unlike the first approach, this is type-safe. Unlike a range or span, this has been available since C++11. Note that you would not actually want to return an incomplete type with no array bound—that’s just begging for a buffer overflow vulnerability—unless your client knew the size of the array by some other means. It would primarily be useful for fixed-size arrays.
Subranges
A good alternative to std::span
is a std::ranges::subrange
, which you can specialize on the const_iterator
member type of your data. This is defined in terms of a begin and end iterator, rather than an iterator and size, and could even be used (with modification) for a container with non-contiguous storage.
This works in GCC 11, and with clang 14 with -std=c++20 -stdlib=libc++
, but not all other compilers (as of 2022):
#include <ranges>
#include <vector>
class Data
{
private:
using DataType = std::vector<int>;
DataType primaryData;
using IndexType = std::vector<DataType::pointer>;
IndexType index;
public:
/* The types of views of primaryData and index, which cannot modify their contents.
* This is a borrowed range. It MUST NOT OUTLIVE the Data, or it will become a dangling reference.
*/
using DataView = std::ranges::subrange<DataType::const_iterator>;
// This disallows modifying either the pointers in the index or the data they reference.
using IndexView = std::ranges::subrange<const int* const *>;
/* According to the C++20 standard, this is legal. However, not all
* implementations of the STL that I tested conform to the requirement that
* std::vector::cbegin is contstexpr.
*/
constexpr DataView getPrimaryData() const noexcept
{
return DataView( primaryData.cbegin(), primaryData.cend() );
}
constexpr IndexView getIndex() const noexcept
{
return IndexView( index.data(), index.data() + index.size() );
}
};
You could define DataView
as any type implementing the range interface, such as a std::span
or std::string_view
, and client code should still work.