How can I avoid DRY with a const-referee and non-const-referee version of a class?

Viewed 144

I have some class which looks like this:

struct A {
  void *stuff;
  int x;

  int foo() const;
}

And I have some functions which take parameters of this type, e.g.

int bar(A a1, A2 a2);
int baz(A2 a2);

The thing is, not all of these functions actually change any memory in a2.stuff; yet - I can't take a const void* and an int, construct an A and pass it to such functions. Or rather, I can, but only using const_cast<> which is really not the way to live your life. Plus, I could easily get confused and pass my const_cast<>'ed A to a function which actually modifies data through A::stuff.

So, I decided I want a "constant A". Not an A whose fields are immutable - an A through which you don't change what stuff points to.

If A was somehow templated, i.e. if it the short was some kind of T - then no problem, you replace A<void> with A<const void> and Bjarne's your uncle. But... A isn't templated. And I don't want to make it templated, since I don't want A<int>'s or anything like that to exist.

So, what should I do?

The naive approach is to replicate the definition of A, almost, for a const_A:

struct const_A {
  void const *stuff;
  int x;

  int foo() const;
}

struct A {
  void *stuff;
  int x;

  int foo() const;
  operator const_A() const { return const_A { stuff, x }; }
}

but that's repetitive, and if I have 20 methods it's even more annoying to replicate.

Bonus points for a solution without a lot of boilerplate, private members, implementation of all ctors etc.

5 Answers

A solution based on the idea of std::experimental::propagate_const:

namespace detail {
struct const_propagating_void_ptr {
    void* ptr;

    operator void       *()       { return ptr; }
    operator void const *() const { return ptr; }
};
} // namespace detail

struct A {
  detail::const_propagating_void_ptr stuff_;
  int x;

  int foo() const;
  void       * stuff()       { return stuff_; }
  void const * stuff() const { return stuff_; }
};

Pros:

  • Rule of Zero (also known as C++ Core Guideline C.20) FTW.
  • In fact, A and the pointer-like class are just plain old structs!
  • You can reuse the pointer-like class (or even templatize it) elsewhere, so it's more text but not that much.

Cons:

  • const A a1 {my_ptr, 123};
    A a2 {a1};
    *a2.stuff() = 456; // ... and a1's pointer is used for write access :-(
    
    This compiles without warnings. Thanks goes to @AyxanHaqverdili for pointing this out.
  • Can't construct a const A from a const void* and an int (without const_cast'ing).
  • Doesn't actually protect the const-propagator from direct access; but at least such access would have to be explicit. We could disable it by actually using std::experimental::propagate_const, which probably has a protected data member, overrides assignment and move-assignment operators etc.

Q: Why not just have the getters in A, and use a void*?

A: A user of A may easily make the mistake of accessing A::stuff_ instead of A::stuff(). If that were just a void *, the chance of accidentally writing to A::stuff_ would be quite high. But with this solution, to get to the void* you need to write: A::stuff_::ptr, and nobody will write my_a.stuff.ptr instead of my_a.stuff() by mistake.

One solution, which @Eljay suggests in a comment, is to have the mutable A inherit the immutable A, which is a common idiom of Objective-C.

Perhaps something along these lines:

struct const_A {
  const void *stuff_;
  int x;

  const void* stuff() const { return stuff_; }
  int foo() const;
}

struct A : public const_A {
  void* stuff() const { return const_cast<void*>(stuff_); }
}

?

Drawbacks:

  • Can no longer write A{ &my_stuff, 123 }.

Similar to @einpoklum, I was thinking of using a std::variant:

#include <variant>

struct const_A {
  std::variant<void*, const void*> stuff_;
  int x;

  int foo() const;
  void* stuff() { return std::get<0>(stuff_); }
  const void* stuff() const { return std::get<1>(stuff_); }
};

int main(void)
{
    return 0;
}

Something like this can also work, but the template stuff has to be propagated all over the place:

template<bool IsConst> 
struct A
{
    std::conditional_t<IsConst, const void*, void*> _stuff;
    int x;

    int foo() const;
};

The reason why I asked for ability to change the functions is because there can be external non-technical reasons why you may not.

This is what I came up with using seldom used "using" syntax (at least I have not seen it almost ever):

  struct A {
     A(void* stuff, int x) : stuff_(stuff), x_(x) {}
     const void* const_stuff() const { return stuff_; }
     void* stuff() { return stuff_; }
     int foo() const;
     
  private:
     void *stuff_;
     int x_;

  };

  struct const_A : private A {
     const_A(void* stuff, int x) : A(stuff, x) {}

  private:
     using A::stuff;
  public:
     using A::const_stuff;
     using A::foo;
  };

  void doSomethingWith_const_A(const_A& c) {
     const void * stuff = c.const_stuff();
     // void * v = c.stuff(); //won't work
  }

  void doSomethingWith_const_A(const const_A& c) {
     const void * stuff = c.const_stuff();
     //void * v = c.stuff(); //won't work
  }
  void doSomethingWith_A(A& a) {
     void * stuff = a.stuff();
     const void * const_stuff = a.const_stuff(); //okay
  }


  int main(int argc, char* argv[])
  {
     A Aobj(nullptr, 0);
     const_A const_Aobj(nullptr, 0);
     doSomethingWith_const_A(const_Aobj);
     doSomethingWith_A(Aobj);
     doSomethingWith_A(const_Aobj);
  }
Related