12

How to make an adapter class to support both const and non-const underlying data appropriately?

Concrete Example

RigidBody is a class describing physic property of object.
Here is its very simplified version (1D):-

class RigidBody{
    float position=1;
    public: float getPosition()const{ return position;}
    public: void setPosition(float ppos){ position=ppos;}
};

Adapter encapsulates RigidBody.
It provides a-little-distorted functionality to get/set position:-

class Adapter{
    public: RigidBody* rigid; int offset=2;
    public: float getPosition(){
        return rigid->getPosition()+offset;     //distort
    }
    public: void setPosition(float ppos){
        return rigid->setPosition(ppos-offset); //distort
    }
};

I can set position of RigidBody indirectly by using Adapter :-

int main() {
    RigidBody rigid;
    Adapter adapter;  //Edit: In real life, this type is a parameter of many function
    adapter.rigid=&rigid;
    adapter.setPosition(5);
    std::cout<<adapter.getPosition();//print 5
    return 0;
}

Everything works (demo).

Objective

I want create a new function that receives constRigidBody* rigid.
I should be able to read from it (e.g. getPosition()) by using adapter.

However, I don't really know how to do it elegantly.

void test(const RigidBody* rigid){
    Adapter adapter2; 
    //adapter2.rigid=rigid; //not work, how to make it work?
    //adapter2.setPosition(5); //should not work
    //adapter2.getPosition();  //should work 
}

My poor solutions

Solution A1 (2 adapters + 1 widget)

Create a widget :-

class AdapterWidget{
    public: static Adapter createAdapter(RigidBody* a);
    public: static AdapterConst createAdapter(const RigidBody* a);
};

AdapterConst can only getPosition(), while AdapterConst can both get and set.

I can use it like :-

void test(const RigidBody* rigid){
    auto adapter=AdapterWidget::createAdapter(rigid);

It is easy to use.

Disadvantage: Code of AdapterConst and Adapter will be very duplicated.

Solution A2 (+inheritance)

It is an improvement of the previous solution.
Let Adapter (has setPosition()) derive from AdapterConst (has getPosition()).

Disadvantage: It is not concise. I use 2 classes for the single task!
This might seem to be trivial, but in a bigger code-base, it is not fun at all.

Specifically, location of getPosition() will be far-away from setPosition(), e.g in different files.
This causes maintainability problem.

Solution B (template)

Create a template class. There are many ways e.g. :-

  • Adapter<T =RigidBody OR const RigidBody >
  • Adapter<bool=true is const OR false is non-const >

Disadvantage: In every ways, it is inelegant. It is an overkill. (?)
I will suffer from disadvantage of template e.g. everything in header.

Solution C1 (const_cast)

I am trying to avoid it. It is evil.

class Adapter{
    public: RigidBody* rigid; 
    void setUnderlying(const RigidBody* r){
        rigid=const_cast< RigidBody*>(r);
    }
    ....
};

Solution C2 (+manual assert)

I can add some assertion manually.
It just emphasizes how much it is unprofessional :-

    bool isConst;
    void setUnderlying(const RigidBody* r){
        ...
        isConst=true;
    }
    void setUnderlying(RigidBody* r){
        ...
        isConst=false;
    }
    void setPosition(float a){
        if(isConst){ /*throw some exception*/ }
        ....
    }

Solution D (run away)

  • Lazy : change from test(constRigidBody* rigid) to test(RigidBody* rigid).
  • Crazy : change RigidBody::setPosition() to be const.

In either way, my program will not be const-correct any more,
but a single Adapter class would be enough.

Question

Do I really have to do one of these things wherever I encounter the const/non-const pattern?
Please provide a pretty solution. (no full code is required, but I don't mind)

Sorry for the long post.

Edit: In real life, Adapter is a parameter for many function.
It is passed around like a toy.

Most of such functions don't have knowledge about RigidBody, so it is not quite suitable to change from a bundle calling someFunction(adapter) to someFunction(offset,rigidbody).

Community
  • 1
  • 1
javaLover
  • 6,347
  • 2
  • 22
  • 67
  • Instead of having `Adapter` as a class with a member pointing to a `RigidBody` it doesn't manage, why not just have two freestanding functions `void setPostition(RigidBody *body, const float& pos)` and `float getPosition(const RigidBody *body)` ? Even better, pass by RigidBody reference. – j b Apr 27 '17 at 11:06
  • @Jamie Bullock An interesting idea... If so, how the adapter's function (get/set) would be implemented? How it can solve the problem? ....... Do you mean a different design pattern? – javaLover Apr 27 '17 at 11:08
  • Am I missing something or you need type erasure like `std::variant` available since c++17? – W.F. Apr 27 '17 at 11:35
  • @W.F. I have never heard about [`std::variant`](http://en.cppreference.com/w/cpp/utility/variant). Thank. I think this is a very basic (?) question, so I didn't really expect that I need such a luxuriant feature. – javaLover Apr 27 '17 at 11:38
  • 2
    Your code tries to much to adhere to Java conventions and ideas. I don't even think you need an Adapter. – David Haim Apr 27 '17 at 11:42
  • I think boost provides variant before c++17 as well. Luxuriant or not you want some type erasure I guess, as long of course as the information about constness of an underlying object can be supplied at runtime (because template base solution here might be misleading)... – W.F. Apr 27 '17 at 11:43
  • @David Haim possible! It is hard for me to judge what is Java-like because it is inside my heart. .... I want to make usage as easy as possible, and I can't find any thing easier to use than `adapter->getPosition()` (no parameter yea!). – javaLover Apr 27 '17 at 11:47
  • @JamieBullock your free function would also need an offset parameter. I this warrents a class – Caleth Apr 27 '17 at 11:49
  • very simple [example](https://wandbox.org/permlink/vuOz8xkMiOMhPSN9) of `std::variant` testing constness qualifiers of underlying object... – W.F. Apr 27 '17 at 12:06
  • @javaLover yes it's a different pattern, see: http://www.drdobbs.com/cpp/how-non-member-functions-improve-encapsu/184401197 on why to prefer non-member non-friend functions. As Caleth pointed out, there is the offset parameter to deal with, in which case you could make both the functions I mentioned members of `Adapter` and have them take a `RigidBody&` and `const RigidBody&` as their first argument respectively rather than storing a bare pointer on `Adapter` – j b Apr 27 '17 at 12:33
  • @W.F. Thank. It is still hard for me to see its full benefit. AFAIK, it is a kind of union. It provides no assertion. Thus, its pro/con is similar to `const_cast`, right? – javaLover Apr 27 '17 at 12:37
  • @Jamie Bullock That is a legendary link for me. I had read it several times. XD .... By the way, the proposed solution is harder to use. `Adapter::getPosition(const RigidBody& )` is painful. – javaLover Apr 27 '17 at 12:40
  • @javaLover well maybe, but IMO it makes the intent clearer: i) the thing you are getting the position of "is a" `RigidBody` and not an `Adapter` ii) the caller is responsible for the lifetime of the `RigidBody` instance and you don't risk a dangling pointer – j b Apr 27 '17 at 12:53
  • 1
    @javaLover the getter could benefit from visitor pattern while setter would test non const at run-time ... [example](https://wandbox.org/permlink/623dFusc6hAk8LHt) – W.F. Apr 27 '17 at 13:26
  • @W.F. Now, I feel it can be a superior improvement from C2 (better encapsulated). I can't access to C++17 in real life, so I've got to find a way without it. After read it several times, I got some rough new idea. (Wasting CPU from runs-time check is not too bad, because it can be disabled by creating a custom macro.) Thank a lot. – javaLover Apr 28 '17 at 02:05
  • 1
    @javaLover if you plan to use it where the run-time efficiency do matter you could also consider storing both `RigidBody* rigid;` and `const RigidBody* c_rigid; ` and overload `rigid` setter for const version which sets `rigid=nullptr;`. Now in position value getter you would use the const field and in setter the non-const one (which checks for rigid existance first)... – W.F. Apr 29 '17 at 11:45
  • If your solutions are complete and correct, this question may be better posed over at [codereview.se] as a `[comarative-review]`. Be sure to read [A guide to Code Review for Stack Overflow users](//codereview.meta.stackexchange.com/a/5778) first, as it's a little different over there! – Toby Speight May 08 '17 at 17:05
  • @Toby Speight Thank, I will keep that in mind. – javaLover May 09 '17 at 03:06

4 Answers4

8

You shouldn't keep with that idea. This is C++, not Java.

Your code is extremely Java oriented. I can see it by the way you write the code, use pointers and silently omit const when needed.

In fact, most of the bad C++ code that I personally saw is pretty much either written to be "C inside classes" or "Java without GC". Both of them are extremely bad ways of writing C++ code.

Your question has an idiomatic solution:

  1. Ditch most of the design patterns. they are useful for languages where object are reference types by default. C++ prefers most of the times to tread object as value types and prefer static polymorphism (templates) rather than run-time polymorphism (inherit + override).

  2. Write two classes , one is Adapter and one is ConstAdapter. This is what the standard library already does. every container has different iterator and const_iterator implementation exactly because of that reason. you can either store something by pointer, either by const pointer. It is error prone trying to mix the two. If there were a nice solution for that problem, we wouldn't have two iterator typedefs for each container.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
David Haim
  • 25,446
  • 3
  • 44
  • 78
  • You are suggesting A1 or A2, right? This makes me sad. ... I am not sure what is "override" here (in a.). There is no virtual function. .... I personally hate template, it increases my re-compile time quite a lot. – javaLover Apr 27 '17 at 11:57
  • @javaLover I was generally talking about templates vs base classes. – David Haim Apr 27 '17 at 11:59
  • @DavidHaim I would argue with `completely no runtime polymorphism in c++` rule. If that was the case for the STL `std::function` wouldn't exist... – W.F. Apr 27 '17 at 12:15
  • @W.F. I never claimed that. – David Haim Apr 27 '17 at 12:26
  • @DavidHaim let me rephrase the comment - I think the power of the c++ is multi-paradigm. If someone finds runtime polymorphism efficient enough in his or her task... I'd say `go ahead!` – W.F. Apr 27 '17 at 12:30
  • @javaLover: You hate recompilation times and I hate runtime errors! And stuff which can be checked at compile time being carried to runtime and bloated exception handlers. C++ has incredibly great static polymorphism, which can ensure that if the program compiles it than runs, runs very fast and without errors! To decrease compile times you should design a good architecture, keeping in mind language idioms. I suggest reading: Large Scale C++ Software Design by John Lakos and these idioms: https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms. Carrying Java to C++ ends up with unmaintainable code. – ovanes May 08 '17 at 18:44
  • @ovanes While I agree with you that runtime errors worse than more recompilation time (partially cured by pimpl), there are 2 things I really dislike about template :- **1)** intellisense/context-clue partially stop working -> partially cure by using some expensive plugin **2)** generally unreadable compile-error message (from my own mistake) -> partially cure by [use other compilers](http://stackoverflow.com/questions/8779521) .... Thank for the book. It looks nice. – javaLover May 09 '17 at 02:33
2

A tightly controlled const_cast looks like a good solution to me:

// Only provides read-only access to the object
struct ConstAdapter {
    int offset = 2;

    // Constructible from const and non-const RigidBodies
    ConstAdapter(RigidBody const *rigid)
    : _rigid{rigid} { }

    // Read-only interface
    float getPosition() {
        return rigid()->getPosition() + offset;     //distort
    }

    // Hidden away for consistency with Adapter's API
    // and to prevent swapping out an "actually non-const" RigidBody
    // for a "truly const" one (see  Adapter::rigid()`).
    RigidBody const *rigid() const { return _rigid; }

private:
    RigidBody const *_rigid;
};

// Inherits read-only functions, and provides write access as well
struct Adapter : ConstAdapter {

    // Only constructible from a non-const RigidBody!
    Adapter(RigidBody *rigid) : ConstAdapter{rigid} { }

    // Write interface
    void setPosition(float ppos){
        return rigid()->setPosition(ppos-offset); //distort
    }

    // Here's the magic part: we know we can cast `const` away
    // from our base class' pointer, since we provided it ourselves
    // and we know it's not actually `const`.
    RigidBody *rigid() const {
        return const_cast<RigidBody *>(ConstAdapter::rigid());
    }
};

Regarding:

Specifically, location of getPosition() will be far-away from setPosition(), e.g in different files. This causes maintainability problem.

This is a non-issue. C++, unlike Java, allows multiple classes in a single file, and you are in fact encouraged to group such tightly related classes together. The declarations of the functions will be only a handful of lines apart, and their definitions can very well be grouped together in the corresponding .cpp file.

Quentin
  • 62,093
  • 7
  • 131
  • 191
1

Use references instead of pointers, and let the const-ness propogate to the Adapter. Then you can safely const_cast, as

template <class Rigid>
class Adapter{
    Rigid & rigid;
    int offset;
public: 
    Adapter(Rigid & rigid, int offset = 2) : rigid(rigid), offset(offset) {}
    float getPosition() const { return rigid.getPosition() + offset; }
    void setPosition(float ppos) { rigid.setPosition(ppos - offset); }
};
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • What is its advantage compared to my C1(const_cast - evil)? – javaLover Apr 27 '17 at 11:21
  • You can't accidentally `setPosition` a const RigidBody, because you can only get const Adapters – Caleth Apr 27 '17 at 11:22
  • I don't think so .... http://coliru.stacked-crooked.com/a/c0f3015d4ed20450 It is compilable .... did I miss something? The `&` might not *propagate* to the field in the way we think? – javaLover Apr 27 '17 at 11:30
  • 1
    I've worked out what is going on there, auto is deducing Adaptor and there is an implicit copy – Caleth Apr 27 '17 at 11:43
  • Yeah, returning a `const Adapter` from your factory method doesn't help, because it's just copy-constructing a non-const local. – Useless Apr 27 '17 at 11:44
  • I was working backwards from `template class Adapter { Rigid & rigid; ... }`. Seems the template is neccecary – Caleth Apr 27 '17 at 11:45
  • Yeah, that's sufficient - you don't even need to do anything clever on `setPosition`, you just won't be able to call it on `Adapter`. – Useless Apr 27 '17 at 11:51
  • @Caleth After the editing, what is its advantage compared to my B? – javaLover Apr 27 '17 at 11:53
  • 1
    brevity, mostly – Caleth Apr 27 '17 at 11:55
1

My first instinct would be to access a RigidBody through an Adaptor and a const RigidBody through a const Adaptor. To achieve this, we use a factory to create the right kind of Adaptor, and in the implementation, we access the underlying RigidBody through an accessor method that (safely) casts only when allowed.

The code:

Start with the private members:

class Adaptor
{
    RigidBody const& body;
    int offset;

    Adaptor(RigidBody body, int offset=2)
        : body(body),
          offset(offset)
    {}

    Adaptor(const Adaptor&) = delete;

The constructor is private, so we can only create instances through our factory. And we remove the copy constructor, so we can't create a non-const Adaptor from a const one.

Next, we have our first overloaded pair - the accessor used in the implementation. I've marked it protected, in case you want a range of similar adaptors. It could safely be public if you want, though.

protected:
    RigidBody& get_body() { return const_cast<RigidBody&>(body); }
    RigidBody const& get_body() const { return body; }

We know that the const_cast is safe, because we only get a non-const Adaptor from a non-const RigidBody, as we see in the other overloaded pair, which is the factory:

public:
    static Adaptor *adapt(RigidBody& body, int offset = 2) { return new Adaptor{ body, offset }; }
    static Adaptor const *adapt(RigidBody const& body, int offset = 2) { return new Adaptor{ body, offset }; }

From here on, each method is declared const unless it needs to modify the body, just as you'd expect. So, if you have a const Adaptor (which you will if you constructed it from a const RigidBody, you can't call any method that modifies body. And the implementation can't modify body from within any of the const methods.

    float getPosition() const
    {
        // this uses the const get_body()
        return get_body().getPosition() + offset;
    }

    void setPosition(float ppos)
    {
        // this uses the mutable get_body()
        get_body().setPosition(ppos-offset);
    }

You can demonstrate the safety by trying to modify body in a const method:

   void illegal() const
   {
       get_body().setPosition(0); // error: passing ‘const RigidBody’ as ‘this’ argument discards qualifiers
       body.setPosition(0);       // error: passing ‘const RigidBody’ as ‘this’ argument discards qualifiers
   }

};

Demo:

With a RigidBody, the adapter allows all operations; with a const RigidBody, the adapter allows only const operations:

#include <memory>
int main()
{
    {
        RigidBody b;
        std::unique_ptr<Adaptor> a{Adaptor::adapt(b)};
        a->setPosition(15.);
        a->getPosition();
    }

    {
        RigidBody const b;
        std::unique_ptr<const Adaptor> a{Adaptor::adapt(b)};
        a->setPosition(15.);     // error: passing ‘const Adaptor’ as ‘this’ argument discards qualifiers
        a->getPosition();
    }
}
Toby Speight
  • 27,591
  • 48
  • 66
  • 103
  • This is the demo of this solution. : http://ideone.com/eedaK6 ..... Can I avoid "new" (i.e. heap)? – javaLover May 09 '17 at 09:17
  • It might be possible - but it's more difficult, as you need to be able to copy (or at least move-construct) the return value from the factory, without losing the constness. I didn't see how to do it, but I'd love to see any suggestions. – Toby Speight May 09 '17 at 09:26
  • Thank. Your solution has some unique strong advantage. .... You can put the demo link (e.g. copy my link) to your question if you want. :) – javaLover May 09 '17 at 09:29