4

A const vector can't be modified as it's a const object. So inserts, appends, erases, are not allowed. However, its contents are not part of that object but are owned by it. As a similar example:

int* const p = new int[10]{1,2,3,4};

p is a const object that owns non-const data which can be modified: p[1]=5;

Vector's operator[] is conditioned on whether vector is const and if so returns a const int& But if the underlying value wasn't const then a const cast removing const should be legal.

To test this I wrote the following program:

#include <vector>

constexpr int foo()
{
    const std::vector<int> v{ 1,2,3 };
    const int a[3]{ 1,2,3 };
    *const_cast<int*>(&v[1]) = 21;

    // However, this should fail and does on GCC and CLANG
    //*const_cast<int*>(&a[1]) = 21;
    return v[1];
}

int main()
{
    constexpr int sb21 = foo();
    const std::vector<int> v{ 1,2,3 };
    *const_cast<int*>(&v[1]) = 21;
    return v[1] + sb21;
}

compiler explorer

MSVC, CLANG, and GCC all compile and execute.

The code evaluates a constexpr function at compile time. Compilers are supposed to produce compile time errors on UB. For comparison if the array, which contains const elements, is uncommented, Clang and GCC both produce errors as expected. However, MSVC does not which appears to be a bug.

Use case is having a fixed size vector that can't be structurally altered but can have contents updated.

std::vector<T> uses std::allocator<T> and so long as the library implementation of vector doesn't use small sizes like std::string's short string optimization then this should be defined behavior.

Here's an example showing how a const std::string exhibits UB for small strings that are stored within the object while longer allocated ones do not:

#include <string>
consteval int foo()
{
    const std::string v{ "1234" };
    //const std::string v{ "123412341234123412341234" };
    *const_cast<char*>(&v[1]) = 'A';
    return v[1];
}
int main()
{
    return foo();
}

Compiler Explorer

Is this defined behavior or are the compilers not flagging UB?

doug
  • 3,840
  • 1
  • 14
  • 18
  • 9
    "Compilers are supposed to produce compile time errors on UB" is false. Unless explicitly specified undefined behavior requires no diagnostic. That what *undefined* means. [Undefined, unspecified and implementation-defined behavior](https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior) – Retired Ninja May 10 '22 at 02:46
  • 3
    *"The code evalutes a constexpr function at compile time."* -- no, the compiler *may* evaluate a `constexpr` function at compile time. You are required to follow certain restrictions so that the function can be evaluated at compile time, but the compiler is under no obligation to do so. (The requirement for the compiler is that it must allow the function in locations where compile-time constants are required. At that point, the compiler is very likely to evaluate at compile time, but still not strictly required to.) – JaMiT May 10 '22 at 02:54
  • *"Use case is having a fixed size vector that can't be structurally altered but can have contents updated."* -- so basically anywhere you could use a `std::array` instead of a `std::vector`? Well, assuming that the size of the array is not a problem for the call stack, I guess. *No, I see no need to come up with another use case. I'd just take the question at a more abstract level: can one assume that object owned by a `const` object are not themselves `const` objects?* – JaMiT May 10 '22 at 03:03
  • @JaMiT const vectors, unlike arrays, can be sized at the point of creation. I also like the fact one has to go out of the way to change data in it. So it can be used normally but updating elements requires a specific intent. – doug May 10 '22 at 03:11
  • @JaMiT A constexpr function is required to be evaluated at compile time if you initialize a constexpr variable by calling it as is done here. – doug May 10 '22 at 03:14
  • @doug *"const vectors, unlike arrays, can be sized at the point of creation."* -- you used `constexpr` in your example, requiring that the size be known at compile time. If your example and your use case differ on this point, you have a potential hole in your case. I'm not saying there is one, but be careful. – JaMiT May 10 '22 at 03:25
  • @doug *"A constexpr function is required to be evaluated at compile time if you initialize a constexpr variable by calling it as is done here."* -- I believe this is false, but I do not have a good reference. A lot of stuff falls under the "as-if" rule. As a thought exercise, suppose someone wrote a C++ compiler that worked by converting C++ code to Python, then packing the result with a Python interpreter to get an executable file. The result could support evaluating all normally compile-time constructs at runtime. Bad for efficiency, but as far as I know compliant with the standard. – JaMiT May 10 '22 at 03:30
  • @JaMiT I used `foo` because constexpr functions are very good, but not perfect, at detecting UB. The use case is a general one I don't see much use inside constexpr functions. – doug May 10 '22 at 03:31
  • For an example where this would be nice to have look at sort. You want sort to swap values in the vector but not change the structure of the vector. – Goswin von Brederlow May 10 '22 at 17:05
  • 1
    I believe the fact that a const vector has const elements is historical. In the olden days when you made a std::vector then T had to have a default constructor because internally when the vector is resized the new elements had to be initialized. And then the old objects were copied into the new array. But for std::vector you can't copy the elements on resize, can write to a const T. So they made const vector to mean const vector semantically or you wouldn't be able to protect the members of a vector from mutation at all when passing vectors. – Goswin von Brederlow May 10 '22 at 17:13

3 Answers3

6

But if the underlying value wasn't const then a const cast removing const should be legal.

This is the weak point of your argument. It's not the underlying value that matters, but the owned object. If the owned object is not a const object, then removing the cast should be legal. However, can you prove that the owned object is not const?

I believe you cannot. Take your own example – a vector containing three ints. Hypothetically, suppose each int is 4 bytes, so the total data is 12 bytes. Also suppose the size of a vector is 24 bytes (allowing 8 bytes for each of a pointer, size, and capacity). It would not be unreasonable to optimize a bit and store the three ints in the vector itself, along with a flag to say that the data is inside the vector instead of being dynamically allocated (a similar approach is used in short string optimization).

Now that we have the possibility that the data is inside the vector itself, we have the possibility that the data is part of a const object, because the vector is const. Casting away this constness to change a value is undefined behavior.

The bottom line is that if you do not own the object, you cannot know for sure how it was created. If the owner tells you it is const, then you have to treat it as const.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • That's probably the best argument and it would absolutely apply to `string` since short string optimization places small strings within the object itself. I don't see that happening with vector for many reasons. However, your second point that it's possible that that the owned data is created as const is possible. I haven't seen that and implementations would have to go out of there way to do so. Probably the only way to guarantee things work in the future would be to just implement it with unique_ptr. – doug May 10 '22 at 03:38
  • 1
    Just checked with `string` and for short strings CLANG and GCC produce compile time errors (attempting to modify a const) but all work when the strings are long. Pretty much as expected. The constexpr was detecting attempts to modify the object where short strings exist but was happy with long strings which says they were not stored in const chars. – doug May 10 '22 at 03:56
  • I don't buy this argument at all. const objects can have mutables so things inside the body of a const object can change just fine. You can also easily implement a vector that is structualy const but mutable contents by inheriting from std::vector and overloading as needed. The fact that a const std::vector::operator [] returns const objects is a design decision that isn't mandated by anything in the C++ language. – Goswin von Brederlow May 10 '22 at 17:04
  • 1
    Actually, the vector's exception guarantee precludes it from using space internal to the object for anything that could throw. So, since `std::allocator` doesn't allocate const Ts, it should be OK to alter values inside a const vector. – doug May 10 '22 at 18:59
  • @GoswinvonBrederlow *"I don't buy this argument at all."* -- I think you do not understand the argument. I showed that there is a possibility that modifying a `const` vector's elements could be undefined behavior. Hence one cannot assume that such a modification is *always* permitted. Your counter-argument is that there is a possibility that modifying those elements is not problematic. This is not a contradiction. In terms of abstract logic, I presented "sometimes X", and you presented "sometimes not X". The conclusion from these two is "not always" rather than "one of these is wrong". – JaMiT May 10 '22 at 23:32
  • @JaMiT Where in your example is there anything more UB than without optimization? Lets assume the ints are inside the vector and you modify them by casting away const. The behavior is still *as if* the vector had a pointer to the head containing the ints. Nothing but the ints changed. Your argument that casting away the constness is UB applies to a vector without short data optimization just the same. But that is only because `operator[]` returns a const. You could just as well define a vector class where a const vector returns non const elements for `operator[]`. – Goswin von Brederlow May 11 '22 at 10:51
  • @GoswinvonBrederlow *"The behavior is still* as if *the vector had a pointer to the head containing the ints."* -- not necessarily, which is the point. For example, a big difference occurs if the vector is placed in a read-only memory chunk. -- *"Your argument that casting away the constness is UB applies to a vector without short data optimization just the same"* -- No, my argument relies on "the data is inside the vector itself" which is false without the optimization. -- *"You could just as well define a vector class where [...]."* -- true, but such a class could not be named `std::vector` – JaMiT May 11 '22 at 11:33
  • @Jamit Except the same happens when the data the vector points to is placed in a read-only memory chunk. A vector in read-only memory can't allocate data on the heap, it must have the data placed in the binary and then only the .data section makes sense. So again, exact same behavior. – Goswin von Brederlow May 11 '22 at 12:12
  • @GoswinvonBrederlow *"the same happens when the data the vector points to is placed in a read-only memory chunk"* -- Correct. If the vector's data is placed in a read-only memory chunk, then you get the same undefined behavior as when the optimization is used and the vector itself is in a read-only chunk. This is another reason why `const_cast` should not be used here. (I had thought about using this example in my answer, but opted for an example with a straight-forward, system-agnostic analysis. Same conclusion, different argument.) – JaMiT May 11 '22 at 22:35
0

Compilers are supposed to produce compile time errors on UB.

The above statement is incorrect unless explicitly specified by the standard.

Can contents of a const vector be modified w/o UB?

No. Trying to modify a const variable leads to undefined behavior.

From dcl.type.cv:

Except that any class member declared mutable can be modified, any attempt to modify a const object during its lifetime results in undefined behavior.

(end quote)


Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior. The program may just crash.

So the output that you're seeing(maybe seeing) is a result of undefined behavior. And as i said don't rely on the output of a program that has UB. The program may just crash.

So the first step to make the program correct would be to remove UB. Then and only then you can start reasoning about the output of the program.


1For a more technically accurate definition of undefined behavior see this where it is mentioned that: there are no restrictions on the behavior of the program.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • The `sizeof(v)` is the size, in bytes, of the vector object. The vector owns allocated space elsewhere but that ownership doesn't mean it's part of the object any more than the memory associated with the pointer to the `new` int array is part of `p` – doug May 10 '22 at 03:28
  • 1
    Actually, all forms of UB in the language are required to be caught when evaluating a constant expression (though UB in the standard library is not required to be caught). It's only runtime UB where anything can happen. – cigien May 10 '22 at 04:53
  • 1
    @cigien Ok, can you give me some reference from the standard where i can confirm and read more about *"all forms of UB in the language are required to be caught **when evaluating a constant expression**"* and then i will delete my answer. – Jason May 10 '22 at 04:55
  • http://eel.is/c++draft/expr.const#5.8 Also see https://en.cppreference.com/w/cpp/language/constant_expression where it's phrased slightly differently, but says the same thing. – cigien May 10 '22 at 04:59
  • 1
    @cigien I know i am still missing something to understand the linked statements. It says: *"An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following: an operation that would have undefined behavior as specified in [intro] through [cpp];"* My question is, how does this imply that UB must be caught when evaluating a constant expression? I mean all this imply is that if evaluation leads to UB then it is not a core constant exprn. Can you maybe explain a little more. – Jason May 10 '22 at 05:30
  • @cigien continued... or do you recommend asking a separate question for this. – Jason May 10 '22 at 05:31
  • 1
    @cigien This is not correct. It isn't a `constexpr` unless it *doesn't* have UB. – user207421 May 10 '22 at 09:39
  • Note: a follow up [question has been posted](https://stackoverflow.com/questions/72183253) based on the comments on this answer. – cigien May 10 '22 at 15:45
-1

const_cast from const T& to T& is not undefined behavior unless the underlying data is itself const.

AFAICT, there is no difference between vector<int> and const vector<int> in that aspect: in both cases it is not UB to const_cast v[0] to int&.

I see other answers here that speculate otherwise, but I don't see any evidence of that in the standard.

See also: Modifying element of const std::vector<T> via const_cast

Also see a similar discussion here: const vector implies const elements?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Ayal
  • 1
  • 1