c++undefined-behaviorconst-correctness

Tracking All Instances While Preserving Const Correctness


There are situations where tracking all instances of a class in a container is desirable. There are existing discussions about this on Stack Exchange (e.g. 1, 2, 3). While dealing with this on my own, I realized that automatically registering instances in a container is prone to undermine const correctness.

This is due to the fact that during construction the this pointer is always considered to be non-const. As far as I know, there is no way to determine whether the currently constructed instance is declared constant or not. Consider this (minimal) example, which compiles fine and succeeds with writing 555 to inst1 without even flinching:

#include <forward_list>

template <typename TInstance>
class ObjList
{
public:
    ObjList()
    {
        instances().push_front(static_cast<TInstance*>(this));
    }

    // Guard against SIOF
    static std::forward_list<TInstance*>& instances()
    {
        static std::forward_list<TInstance*> instances;
        return instances;
    }
};

class SomeClass : public ObjList<SomeClass>
{
public:
    SomeClass(int someData) :
        someData{someData}
    {}

    void write(int data)
    {
        someData = data;
    }

private:
    int someData;
};

SomeClass inst0{200};
const SomeClass inst1{400};

int main()
{
    for (SomeClass* instance : ObjList<SomeClass>::instances())
    {
        instance->write(555);
    }
}

Or the more extensive example on godbolt. Others have pointed out that this is just another way to shoot yourself into the foot and a source of undefined behavior (see 4).

The primary questions are:

If there is no way to fix this, I am even OK with the fact that users can modify instances that have been declared constant via a detour (there is going to be documentation on the topic). The remaining question is then:


Solution

  • If I enforce that all instances are put in a writable memory location (e.g. through adding a mutable member in ObjList), can I continue to use the outlined approach or will this still result in undefined behavior?

    [dcl.type.cv] states that you may modify mutable members of const objects but modifying const members is undefined behaviour.

    Can you recommend me a different design to look into?

    You could force instances of your classes to be mutable by declaring them as members of a wrapper class rather than as derived classes.

    template <typename T>
    class Wrapper
    {
    private:
        mutable T value;
    
    public:
        static std::forward_list<T*>& instances()
        {
            static std::forward_list<T*> instances;
            return instances;
        }
    
        template <typename... Args>
        explicit Wrapper(Args&&... args)
        : value(std::forward<Args>(args)...)
        {
            instances().push_front(&value);
        }
    
        Wrapper(Wrapper const&) = delete;
        Wrapper& operator=(Wrapper const&) = delete;
    
        T const& get() const { return value; }
        T& get() { return value; }
    };
    

    You could make the constructor of the wrapped class private to force the use of Wrapper<T>.

    class SomeClass
    {
        friend class Wrapper<SomeClass>;
    
    private:
        SomeClass(int);
    };