Is modifying the internal bytes of a const object undefined behavior in case it contains another object constructed by placement new?
Asked Answered
T

3

8

Consider the following example:

#include <new>

struct FunctionObject
{
    int operator()() // non-const, modifies the state of this object
    {
        i += 1;
        j += 2;
        return i + j;
    }

    int i = 0;
    int j = 0;
};

struct Wrapper
{
    explicit Wrapper(const FunctionObject& input_object)
    {
        constructed_object = ::new (buffer) FunctionObject(input_object);
    }

    ~Wrapper()
    {
        constructed_object->~FunctionObject();
    }

    int operator()() const // const, but invokes the non-const operator() of the internal FunctionObject
    {
        return (*constructed_object)(); // this call modifies the internal bytes of this Wrapper
    }

    alignas(FunctionObject) unsigned char buffer[sizeof(FunctionObject)];
    FunctionObject* constructed_object = nullptr;
};

int test()
{
    const FunctionObject function_object{3, 4};
    const Wrapper object_wrapper{function_object}; // this call modifies the internal bytes of a const Wrapper
    return object_wrapper();
}

A Wrapper contains an internal FunctionObject which is constructed inside the Wrapper by a placement new.

The Wrapper object is const, its operator() is also const, but calling it causes the internal state of the object to be modified. In many cases, similar scenarios are undefined behavior in C++.

The question is, is it undefined behavior in this particular case (~ do I need to mark the buffer as mutable?), or does the C++ standard allow writing code like this?

Toggle answered 20/10, 2020 at 15:49 Comment(3)
I don't see why it should not be UB.Illative
This is essentially the same as a const object that saves a non-const pointer to itself during construction, right?Commissionaire
@user253751 Kind of. With the difference that this const object constructs another (non-const) object within itself, and modifies that other object after construction. I.e., Wrapper modifies FunctionObject, but not itself directly, though that FunctionObject happens to be located inside the Wrapper's internal bytes. Thus the ambiguity.Toggle
C
4

This is Undefined Behavior.

From [dcl.type.cv],

Any attempt to modify a const object during its lifetime results in undefined behavior.

Adding the mutable specifier to buffer will allow it to be modified by a const member function.

Cruiser answered 20/10, 2020 at 16:47 Comment(0)
S
1

From [class.mfct.non-static.general/4]:

A non-static member function may be declared const [...]. These cv-qualifiers affect the type of the this pointer. They also affect the function type of the member function; a member function declared const is a const member function [...].

Then from [class.this/1]:

The type of this in a member function whose type has a cv-qualifier-seq cv and whose class is x is "pointer to cv x". [Note 1: Thus in a const member function, the object for which the function is called is accessed through a const access path. -- end note]

And eventually from [dcl.type.cv/3-4]:

[...] a const-qualified access path cannot be used to modify an object [...]

Any attempt to modify a const object during its lifetime results in undefined behavior.

Hence, your test() routine shows undefined behavior, and that even if your Wrapper object object_wrapperwasn't const.

Summerly answered 25/10, 2020 at 0:45 Comment(0)
R
0

No. It won't be undefined behaviour from practice, forget about what "standard" describes. It will behave just like without the "const".

The reason is that the "const" qualifier for an object in C++ is merely to tell the compiler to generate a compilation error in the case where some members or functions inside the object get invoked. Besides that, adding a "const" qualifier to an object doesn't results in different binaries after compilation.

This is different from the "const" qualifier applied on primitive types, such as "const int", "const char *". A variable of type "const int" will probably replaced by its compile-time value for optimization purpose. A string literal of type "const char *" will refer to some memory page that has read access limitation by the OS in such a way that modifying the memory content will let to crash of the program.

Also, a "const" qualifier of a function does result in different function signatures and can be treated like a function overloading.

Rufe answered 29/10, 2020 at 9:22 Comment(1)
thanks, but I can't agree with this: 'forget about what "standard" describes' -- I'd prefer not to, if the cost is nothing but adding one extra word (mutable) to just one line of code, and this way everyone will be happyToggle

© 2022 - 2024 — McMap. All rights reserved.