'How can I propagate const when returning a std::vector<int*> from a const method?

Lets show it in an example where we have a Data class with primary data, some kind of index that points to the primary data, and we also need to expose a const version the index.

class Data
{
public:
  const std::vector<int>& getPrimaryData() const { return this->primaryData; }
  const std::vector<int*>& getIndex() const { return this->index; }
private:
  std::vector<int> primaryData;
  std::vector<int*> index;
};

This is wrong, as the user can easily modify the data:

const Data& data = something.getData();
const std::vector<int*>& index = data.getIndex();
*index[0] = 5; // oups we are modifying data of const object, this is wrong

The reason of this is, that the proper type the Data::getIndex should be returning is:

const std::vector<const int*>&

But you can guess what happens when you try to write the method that way to "just convert the non-const variant to const variant":

// compiler error, can't convert std::vector<int*> to std::vector<const int*> these are unrelated types.
const std::vector<const int*>& getIndex() const { return this->index; }

As far as I know, C++ doesn't have any good solution to this problem. Obviously, I could just create new vector, copy the values from the index and return it, but that doesn't make any sense from the performance perspective.

Please, note that this is just simplified example of real problems in bigger programs. int could be a bigger object (Book lets say), and index could be index of books of some sort. And the Data might need to use the index to modify the book, but at the same time, provide the index to read the books in a const way.



Solution 1:[1]

You're asking for std::experimental::propagate_const. But since it is an experimental feature, there is no guarantee that any specific toolchain is shipped with an implementation. You may consider implementing your own. There is an MIT licensed implementation, however. After including the header:

using namespace xpr=std::experimental;
///...
std::vector<xpr::propagate_const<int*>> my_ptr_vec;

Note however that raw pointer is considered evil so you may need to use std::unique_ptr or std::shared_ptr. propagate_const is supposed to accept smart pointers as well as raw pointer types.

Solution 2:[2]

In C++20, you can just return a std::span with elements of type const int*

#include <vector>
#include <span>

class Data
{
public:
  std::span<const int* const> getIndex() const { return this->index; }
private:
  std::vector<int*> index;
};

int main() {
  const Data data;
  const auto index = data.getIndex();
  *index[0] = 5;  // error: assignment of read-only location
}

Demo

Solution 3:[3]

Each language has its rules and usages... std::vector<T> and std::vector<const T> are different types in C++, with no possibility to const_cast one into the other, full stop. That does not mean that constness is broken, it just means it is not the way it works.

For the usage part, returning a full container is generally seen as a poor encapsulation practice, because it makes the implementation visible and ties it to the interface. It would be better to have a method taking an index and returning a pointer to const (or a reference to a pointer to const if you need it):

const int* getIndex(int i) const { return this->index[i]; }

This works, because a T* can be const_casted to a const T *.

Solution 4:[4]

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.

Solution 5:[5]

You could return a transforming view to the vector. Example:

auto getIndex() const {
    auto to_const = [](int* ptr) -> const int* {
        return ptr;
    };
    return this->index | std::views::transform(to_const);
}

Edit: std::span is simpler option.


If index contains pointers to elements of primaryData, then you could solve the problem by instead storing integers representing the indices of the currently pointed objects. Anyone with access to non-const primaryData can easily turn those indices to pointers to non-const, others cannot.

primaryData isn't stable,

If primaryData isn't stable, and index contains pointers to primaryData, then the current design is broken because those pointers would be invalidated. The integer index alternative fixes this as long as the indices remain stable (i.e. you only insert to back). If even the indices aren't stable, then you are using a wrong data structure. Linked list and a vector of iterators to the linked list could work.

Solution 6:[6]

As mentioned in a comment, you can do this:

class Data
{
public:
  const std::vector<int>& getPrimaryData() const { return this->primaryData; }
  const std::vector<const int*>& getIndex() const { return this->index; }
private:
  std::vector<int> primaryData;
  std::vector<const int*> index;
  int* read_index_for_writing(std::size_t i) { return const_cast<int*>(index[i]); }
};

Good things about this solution: it works, and is safe, in every version of the standard and every compliant implementation. And it returns a vector reference with no funny wrapper classes – which probably doesn't matter to the caller, but it might.

Bad: you have to use the helper method internally, though only when reading the index for the purpose of writing the data. And the commenter described it as "dirty", but it seems clean enough to me.

Solution 7:[7]

Prepare the type like following, and use as return type of Data::getIndex().

class ConstIndex
{
private:
  const std::vector<int*> &index;
public:
  ConstIndex( const std::vector<int*> &index ) : index(index) {}

public:
  //Implement methods/types needed to emulate "const std::vector<const int*>"
  const int *operator[]( size_t i ) const { return index[i];    }
  const int *at( size_t i ) const { return index.at(i); }
  ...
};

Solution 8:[8]

Here is an ugly solution that works with versions before C++20 using reinterpret_cast:

const std::vector<const int*>& getIndex() const{ 
    return reinterpret_cast<const std::vector<const int*>&>(data); 
}

Note this does actually return a reference bound to an lvalue, not a const& bound to an rvalue:

std::vector<const int*>& getIndex() const{ 
    return reinterpret_cast<std::vector<const int*>&>(data); 
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ComicSansMS
Solution 2 康桓瑋
Solution 3 Serge Ballesta
Solution 4 Toby Speight
Solution 5
Solution 6 benrg
Solution 7 fana
Solution 8 Captain Hatteras