7

I would like to create a const std::vector<T> that can store updateable values that user code can access but not (easily) modify. There are several advantages to this. The vector object (but not its contents) cannot be changed. So references to entries in the vector can be established without risk of dangling if someone decides later to add new elements or other operations that can cause dangling references. And, since it would be a complete const object, it cannot be modified even using placement-new without UB and/or compiler complaints.

This seems to be possible because, while the vector object is const, the Ts are not and must be stored as non-consts. Attempting to store them as const produces the following error:

The C++ Standard forbids containers of const elements because allocator<const T> is ill-formed.

See this

So, since the T's are not const, but are only made to appear const when accessed, it appears that they may be accessed and updated by using a const_cast to remove the const.

I haven't run across this use of modifiable const vectors, but it appears to be quite legal. Am I missing something?

Here's the code including constexpr for additional UB testing:

#include <vector>
#include <iostream>

constexpr int foo()
{
    const std::vector<int> v{ 1,2,3 };
    const int& rci = v[0];              // A const ref to v[0] is ok
    int& ri = const_cast<int&>(v[0]);   // A ref to v[0] as a non-const requires a cast
    ri = 42;                            // v[0] is now 42;
    return v[0];
}

void update_const_v(const std::vector<int>& v)
{
    for (const int& i : v)
        const_cast<int&>(i) = i + 1;
}

void print(const std::vector<int>& v)
{
    for (auto& i:v)
        std::cout << i << '\n';
    std::cout << '\n';
}

int main()
{
    const std::vector<int> v{ 1,2,3 };
    const int& ri = v[0];   // A ref to v[0]
    print(v);
    update_const_v(v);
    print(v);
    std::cout << "Reference to first element of const vector: " << ri << '\n';

    // Check for UB using constexpr
    constexpr int i = foo();
    return i;
}

And here's it running in msvc, clang, and gcc.

doug
  • 3,840
  • 1
  • 14
  • 18
  • Have you considered using a std::span? – Jacob Merson Sep 04 '22 at 04:10
  • @JacobMerson Use case is a widely used dataset that is rarely changed and only by a specific function. But I want it to have the safety of constness in the bulk of the code. I like span for some uses but updating the vector is pretty easy. I just const cast to a T* and index it. – doug Sep 04 '22 at 04:41
  • 1
    Even if it’s legal, why not just provide an interface that exposes const reference to the vector to the outside world instead? Effectively it should be the same with a way clearer intent. – alagner Sep 04 '22 at 05:18
  • 1
    Modifying an object that is defined as `const vector` is undefined behavior. A `const_cast` does not change that. – j6t Sep 04 '22 at 07:38
  • 1
    @j6t while it's true modifying an object originally const is not allowed. This only applies to the object, not data it may refer to. Example: `int * const p {new int{}};` p is const but the int it refers to is not. – doug Sep 04 '22 at 14:14
  • @alagner Workarounds are needed if not legal. However, if legal, which it appears to be, provides the expected `std::vector` methods to the coder including the default constification of accesses. Desirable since I don't want accidental changes. – doug Sep 04 '22 at 14:23
  • Unfortunately seeing it run properly on multiple compilers doesn't prove anything. You'd need to see supporting language in the C++ spec. I think the spec does allow for casting away constness if the original object is non-const, but I wouldn't know where to find it. – Mark Ransom Sep 04 '22 at 18:22
  • @MarkRansom e.g. [point 4 dcl.type.cv](https://eel.is/c++draft/dcl.type.cv). But again, as j6t has written, this does not change a fact that casting away constness on an initially const object and acessing it like that is UB. Sure, it's likely to work due to implementation...until it stops ;) – alagner Sep 04 '22 at 22:29
  • @alagner My point is that the T is not const. It's only seen as const since v[i] returns a reference to a const, essentially casting a non const to a const. That is reversable. Hence T can be modified. just not though v[i]. Your [example](https://eel.is/c++draft/dcl.type.cv#4) showing reversion of a const cast is pretty specific. – doug Sep 05 '22 at 00:17
  • @alagner thanks for the link. Point 4 only applies if the variable backed by the reference is `const`. In this case I think we can establish that it won't be, so the relevant part would be the 3rd section of example 1 which is shown to be OK. – Mark Ransom Sep 05 '22 at 02:56

1 Answers1

1

The only plausible implementation of std::vector that I'm aware of uses a pointer to an array of T. Since your T is non-const, the objects can be safely modified, the same as if you used const std::unique_ptr<T[]>.

This isn't an ironclad guarantee that someone couldn't write a std::vector implementation which works differently, e.g. it could collude with the compiler to place "const vector" data into read-only memory.

So I think you're left with "It should work and not invoke UB, unless a platform does something very unusual." If you're trying to write 100% portable code, I wouldn't make this assumption, but for most practical purposes it seems valid, if a bit weird.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • It's a guarantee for vector's standard allocator which is what's being used. [See this](https://timsong-cpp.github.io/cppwp/allocator.requirements#general-2.1) One can't create vectors with either const and/or volatile T's according to the standard. – doug Sep 04 '22 at 16:33
  • You're assuming the allocator is actually used. It doesn't have to be: https://stackoverflow.com/questions/70645211/why-is-it-allowed-for-the-c-compiler-to-opmimize-out-memory-allocations-with-s What I'm saying is that the library and compiler authors could collude to eliminate the new/malloc we're all expecting, if they can generate code that behaves as if the vector data was allocated. For example the vector could point into the `.rodata` section. – John Zwinck Sep 04 '22 at 16:47
  • Sure the optimizer can elide allocation under the as-if rule. But if it did, and the T's were altered via an update, that would also have to be elided to produce the correct result with the as-if rule. Since T's may not be const, they can be altered w/o UB. – doug Sep 04 '22 at 17:16
  • Yeah, I don't think such an optimization would actually be implemented anyway, this danger notwithstanding. – John Zwinck Sep 04 '22 at 17:22