2

I have the following code which seems to work always (msvc, gcc and clang).

But I'm not sure if it is really legal. In my framework my classes may have "two constructors" - one normal C++ constructor which does simple member initialization and an additional member function "Ctor" which executes additional initialization code. It is used to allow for example calls to virtual functions. These calls are handled by a generic allocation/construction function - something like "make_shared".

The code:

#include <iostream>

class Foo
{
    public:
      constexpr Foo() : someConstField(){}
    public:
        inline void Ctor(int i)
        {
            //use Ctor as real constructor to allow for example calls to virtual functions
            const_cast<int&>(this->someConstField) = i;
        }
    public:
      const int someConstField;
};

int main()
{
    //done by a generic allocation function
    Foo f;
    f.Ctor(12); //after this call someConstField is really const!

    //
    std::cout << f.someConstField;
}
LIU Qingyuan
  • 524
  • 5
  • 18
Bernd
  • 2,113
  • 8
  • 22
  • 1
    Does this answer your question? [Is const\_cast safe?](https://stackoverflow.com/questions/357600/is-const-cast-safe) – Jan Schultke Aug 18 '20 at 09:34
  • If having a seperate `Ctor` function a common pattern in your code, you should consider having the allocation function not construct the object and using placement `new` instead of calling the `Ctor` method. – IlCapitano Aug 18 '20 at 09:48
  • It does allocate aligned non-const raw memory, does placement new and calls the Ctor function if one exists – Bernd Aug 18 '20 at 09:50
  • @Bernd Yes, you can do `new (raw_memory) Foo{i};`, which returns a pointer to the constructed object. Refer to the _Placement new_ section on [cppreference](https://en.cppreference.com/w/cpp/language/new). – IlCapitano Aug 18 '20 at 09:52

5 Answers5

2

Modifying const memory is undefined behaviour. Here that int has already been allocated in const memory by the default constructor.

Honestly I am not sure why you want to do this in the first place. If you want to be able to initalise Foo with an int just create an overloaded constructor:

...
    constexpr Foo(int i) : someConstField{i} {}

This is completely legal, you are initalising the const memory when it is created and all is good.

If for some reason you want to have your object initalised in two stages (which without a factory function is not a good idea) then you cannot, and should not, use a const member variable. After all, if it could change after the object was created then it would no longer be const.

As a general rule of thumb you shouldn't have const member variables since it causes lots of problems with, for example, moving an object.

When I say "const memory" here, what I mean is const qualified memory by the rules of the language. So while the memory itself may or may not be writable at the machine level, it really doesn't matter since the compiler will do whatever it likes (generally it just ignores any writes to that memory but this is UB so it could do literally anything).

Object object
  • 1,939
  • 10
  • 19
  • It's good you've provided an example of how the same thing might be achieved without UB. The idea of "const memory", however, may be misleading. The standard doesn't have a notion of such things and the compilers won't necessarily put the data into read-only memory. – OMGtechy Aug 18 '20 at 09:35
  • Is it really const memory? – Bernd Aug 18 '20 at 09:39
  • @OMGtechy I think its a pretty good abstraction since whether its actually read-only memory or not is generally not going to be relevent. What do you think would be a better way to describe it? – Object object Aug 18 '20 at 09:40
  • @Firefly true. Perhaps it could be described in terms of compiler assumptions - i.e. the compiler (especially the optimiser) will assume that you haven't changed a `const` value, this assumption is now false and as a result terrible things can happen (UB). – OMGtechy Aug 18 '20 at 09:42
  • @Bernd Its const qualified memory in terms of the language. That means its undefined behaviour to modify it once it has been initialised. Whether at the machine level its actually OK to write to that memory doesn't really matter since its UB so the compiler might decide to rewrite it to make toast (actually generally it just ignores any writes to it) – Object object Aug 18 '20 at 09:43
  • Can you show me the page / paragraph in the standard where it is defined. For me it is very clear for objects but not for subobjects of non-const objects. – Bernd Aug 18 '20 at 10:14
1

No.

It is undefined behaviour to modify a const value. The const_cast itself is fine, it's the modification that's the problem.

OMGtechy
  • 7,935
  • 8
  • 48
  • 83
1

According to 7.1.6.1 in C++17 standard

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

And there is an example (similar to yours, except not for class member):

const int* ciq = new const int (3); // initialized as required
int* iq = const_cast<int*>(ciq); // cast required
*iq = 4; // undefined: modifies a const object
pptaszni
  • 5,591
  • 5
  • 27
  • 43
0

I suggest, that you use the constructor to avoid the const cast. You commented, that after your call of Ctor the value of someConstField will remain const. Just set it in the constructor and you will have no problems and your code becomes more readable.

#include <iostream>

class Foo
{
    public:
      constexpr Foo(int i) : someConstField(Ctor(i)){}

      int Ctor(); // to be defined in the implementation

      const int someConstField;
};

int main()
{
    Foo f(12);
    std::cout << f.someConstField;
}
schorsch312
  • 5,553
  • 5
  • 28
  • 57
  • I'm actually writing a Compiler with C++ as it's backend. The problem is that in the source language arbitrary calculations and calls are allowed in a constructor, So I can't translate the code to such simple init expressions. – Bernd Aug 18 '20 at 09:57
  • 1
    @Bernd Thats a seperate question but sure you can. Just call a function in the initialiser like: `Foo(int i) : someConstField{some_function(i)} {}` – Object object Aug 18 '20 at 09:58
  • @Firefly Or an immediately invoked lambda, where you can put arbitrary code, not just a function call. – IlCapitano Aug 18 '20 at 09:59
  • @IlCapitano Yeah if its generated thats probably better, lambda's in inititalisation lists can get _really_ ugly though if theres too many of them – Object object Aug 18 '20 at 10:00
  • That's a good idea (I did it with lambdas...). But my problem is that I need to allow temporaries, too. – Bernd Aug 18 '20 at 10:00
  • Perhaps the keyword `mutable` is what you are looking for. You can have a const object, where the parameter flagged with mutable can be changed. This keyword should be used with care. – schorsch312 Aug 18 '20 at 10:06
  • 2
    @Bernd You could store the members as tuples and initialize them all at once with a single lambda. – IlCapitano Aug 18 '20 at 10:06
  • @Firefly I updated my answer, regarding your comment – schorsch312 Aug 18 '20 at 10:08
  • @IlCapitano nice idea - but then I would need to change the field access... – Bernd Aug 18 '20 at 10:15
0

If your allocation function allocates raw memory, you can use placement new to construct an object at that memory location. With this you must remember to call the destructor of the object before freeing the allocation.

Small example using malloc:

class Foo
{
    public:
      constexpr Foo(int i) : someConstField(i){}
    public:
      const int someConstField;
};

int main()
{
    void *raw_memory = std::malloc(sizeof(Foo));
    Foo *foo = new (raw_memory) Foo{3}; // foo->someConstField == 3
    // ...
    foo->~Foo();
    std::free(foo);
}
IlCapitano
  • 1,994
  • 1
  • 7
  • 15