When working on a legacy codebase that has leading-edge C++ constructs, but also deeply legacy design decisions, sometimes there’s nifty ways to use the one against the other.

Here is a class with a needlessly public data member. I don’t know who wrote it, git history stops in 2014. At least there’s some modern C++ around it.

class Example
{
public:
    void reset(int x) { std::ranges::fill(needlesslyPublic, x); }

    static constexpr std::size_t publicSize = 8;
    int needlesslyPublic[publicSize] = {};
};

int consumer(Example & e, int index)
{
    return e.needlesslyPublic[index];
}

Now, it so happens that I know that the only place that writes to this needlessly public data member is the member function reset(). Every other access is read-only, like in the function consumer().

Legacy codebases will also have lots of missing const, which is something clang-tidy can help with, and often use int where std::size_t is meant – but those are critiques for the function consumer(), not the Example class.

Now, I’ve got 400 places in the codebase that all access needlesslyPublic for reading, and I don’t feel like causing code-churn in those places. So I want to keep the name and its access specifier (public) intact, while ensuring read-only access from elsewhere. I don’t care about having to recompile things, though.

Narrator: Enter “the totally-bogus data-pointer”.

class Example
{
public:
    void reset(int x) { std::ranges::fill(m_needlesslyPublic, x); }

    static constexpr std::size_t publicSize = 8;
    const int * needlesslyPublic = &m_needlesslyPublic[0];
private:
    int m_needlesslyPublic[publicSize] = {};
};

Thanks to in-class initialization, and the fortune that the needlessly-public thing is an array, I can move the array into private, and re-introduce the public name as a pointer. It’s a legacy code base, we just love array-pointer-decay. So now needlesslyPublic[i] dereferences the pointer, but the dereferenced type is a const int & so it is read-only. The one place that writes to the array can use the private data member, which is writable.

Minimal fuss, easily detects that one other place (there’s always one) in the codebase that writes to the data member, no code-churn in read-only consumers.

Narrator: But did you forget serialization? Copying? Moves?

Ugh. While being clever about one bunch consumers, I totally forgot that this was adding the size of a pointer to each object and that this pointer was meaningless outside of the specific object and its location in memory. Copying an object should copy the contents of m_needlesslyPublic and not change the pointer. The pointer always needs to keep pointing to the array owned by this object.

Rather than going into the weeds of copy- and move-construction (rule of five), I realized that the relevant bit was operator[] on the needlessly public data member.

Narrator: Enter “the array-ish”.

Read-only consumers are only interested in needlesslyPublic[i], so what if I provide an object that has that interface, but read-only? Here is a nested class Hidden that provides such an interface:

class Example
{
public:
    void reset(int x) { std::ranges::fill(needlesslyPublic.data, x); }

    static constexpr std::size_t publicSize = 8;

    class Hidden
    {
    friend class Example;
    public:
        int operator[](std::size_t index) const { return data[index]; }
    private:
        int data[publicSize] = {};
    };
    Hidden needlesslyPublic;
};

I don’t like the friend declaration (nobody likes friends). But it works, in the sense that consumers have no code change, read-only behavior is enforced, and only the methods of class Example can write to the hidden data. It’s like a const data member, but writable!

Narrator: What about non-array data members?

Look mr-know-it-all-narrator, if I really wanted to play silly games with member access, I would write Python and use attributes.

This particular approach works only because all my consumers use some specific syntax – array indexing – which I can capture with operator[]. If the needlessly public data member was some non-array type, then I don’t have any syntax to grab hold of.

For my particular legacy codebase, I ended up using the pointer approach to verify that all (but one) access was read-only, and then rolled it back because the Hidden class example didn’t work well with others. For the foreseeable future I need to rely on process and code reviews to keep this sane.