2

I'm learning C++ while I run into this situation, where I want to implement an equivalently efficient version in C++ of the following symbolic code in C.

<header.h>
struct Obj;
Obj* create(...);
void do_some_thing(Obj*);
void do_another_thing(Obj*);
void destroy(Obj*);

The requirements are:

  • The implementation is provided in a library (static/dynamic) and the header doesn't expose any detail other than the interface
  • It should be equally efficient

Exposing an interface (COM-like) with virtual functions doesn't qualify; that's a solution to enable polymorphism (more than one implementation exposed through the same interface) which isn't the case, and since I don't need the value it brings, I can't see why I should pay the cost of calling functions through 2 indirect pointers. So my next thought was the pimpl idiom:

<header.h>
class Obj {
public:
  Obj();
  ~Obj();
  void do_some_thing();
  void do_another_thing();
private:
  class Impl;
  smart_ptr<Impl>  impl_; // ! What should I use here, unique_ptr<> or shared_ptr<> ?
};

shared_ptr<> doesn't seem to qualify, I would pay for unnecessary interlocked increment/decrement that didn't exist in the original implementation. On the other hand unique_ptr<> makes Obj non-copyable. This means that the client can't call his own functions that take Obj by value, and Obj is merely a wrapper for a pointer, so essentially he can't pass pointers by value! He could do that in the original version. (passing by reference still doesn't qualify: he's still passing a pointer to a pointer)

So what should be the equally efficient way to implement this in C++?

EDIT: I gave it some more thought and I came to this solution:

<header.h>
class ObjRef // I exist only to provide an interface to implementation
{            //  (without virtual functions and their double-level indirect pointers)
public:
  ObjRef();
  ObjRef(ObjRef);           // I simply copy pointers value
  ObjRef operator=(ObjRef); // ...
  void do_some_thing();
  void do_another_thing();
protected:
  class Impl;
  Impl*  impl_; // raw pointer here, I'm not responsible for lifetime management
};

class Obj : public ObjRef
{
  Obj(Obj const&);            // I'm not copyable
  Obj& operator=(Obj const&); // or assignable 
public:
  Obj(Obj&&);                 // but I'm movable (I'll have to implement unique_ptr<> functionality)
  Obj& operator=(Obj&&);
  Obj();
  ~Obj(); // I destroy impl_
  // and expose the interface of ObjRef through inheritance
};

Now I return to client Obj, and If client needs to distribute the usage of Obj by calling some other his functions, he can declare them as

void use_obj(ObjRef o);

// and call them:
Obj o = call_my_lib_factory();
use_obj(o);
el22
  • 21
  • 2
  • 1
    Why would you have to use PIMPL here? Isn't `Obj *obj = new Obj;` enough? –  Mar 10 '13 at 11:51
  • If the user wants to call object by value he expects the original object doesn't change. This also means that `shared_ptr` isn't possible. You have to give a copy-constructor and possibly an assigment constructor. Since you're already writing those you can simply use `std::unique_ptr` for `impl_`. – Zeta Mar 10 '13 at 11:55
  • @H2CO3: why should I allocate on heap a pointer to a pointer? – el22 Mar 10 '13 at 12:16
  • @el22 I don't understand what you mean by that. –  Mar 10 '13 at 12:17
  • 1
    @Zeta: If I return unique_ptr this means that I should declare Impl in the header (in order to expose the functions/interface) and since Impl has data members, I would need to expose the headers that define all those data members – el22 Mar 10 '13 at 12:18
  • @H2CO3: I though you proposed to return new Obj as I have presented it (as opposed to new Impl, for which I commented above) – el22 Mar 10 '13 at 12:21
  • @el22 I mean... Why would you return a private implementation of an object? If all requirement is that you create an object dynamically and then call functions (in C) or methods (in C++) on it, then what I wrote is more than enough. –  Mar 10 '13 at 12:22
  • @H2CO3: I'm not following you... creating new Obj means allocating on heap a pointer to a pointer (since Obj is itself a pointer to impl), and returning new Impl means that I have to declare it (with all its data members in the header, and include all headers where those data members are declared) – el22 Mar 10 '13 at 12:29
  • @el22: What H2CO3 meant is: ___Why would you even want to return your `impl_`___? We never said anything about _that_. – Zeta Mar 10 '13 at 12:30
  • @Zeta: I don't want to return anything more than absolutely necessary, like a token that identifies this instance (typically a pointer), and I don't want to expose anything more than necessary, like the interface to access the features. So what you propose, can you give me a sample? – el22 Mar 10 '13 at 12:35
  • At any rate, a reference counted smart pointer will never have the performance of a raw C pointer. Different tools for different application, with some overlapping of course. – dtech Mar 10 '13 at 15:11
  • Post edit, what advantage is your `Obj`/`ObjRef` implementation trying to achieve over returning a non-copyable `Obj` type with no inheritance and letting the client pass around pointers to the returned object? It seems to obscure the potential danger of slicing an `Obj`. At least when passing a pointer around it's obvious that the referent object must be kept alive for the object to be valid. – CB Bailey Mar 10 '13 at 19:05
  • @Charles Bailey If I collapse Obj/ObjRef into Obj in my edit, then clients are forced to pass pointers or references (that at the machine level are still pointers) to a pointer. There's no reason (other than an artificial obstacle) to impede passing pointers (*impl_) by value. I also don't think it obscures anything - (ObjRef o) is as expressive and typesafe as (Obj& o) (and probably even more explicit) – el22 Mar 10 '13 at 19:40
  • `ObjRef` is _less_ expressive than `Obj&`. You are using inheritance but an object doesn't have an "IS A" relationship with an object reference. Your "useful" functions are on the reference, not on the object but if I slice an `Obj` into an `ObjRef` and let the object die those functions are not usable. Clients being forced to pass pointers or references is a good thing because it makes it obvious what lifetime requirements your objects have. As far as I can see you are not permitting or inhibiting anything over the simpler, conventional solution of passing references to a non-copyable object. – CB Bailey Mar 10 '13 at 21:22

2 Answers2

0

Why not keep the C original? The reason that you didn't have to pay the reference counting premium in the C version is that the C version relied on the caller to keep any tally of the number of copies of Obj* in use.

By trying to ensure that the replacement is both copyable and that ensure that the underlying destroy method is only called once the last reference is destroyed you are imposing additional requirements over the original, so it is only natural that the proper solution (which seems to me to be shared_ptr) has some limited additional expense over the original.

CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • which means that the *whole* program won't be slower when translated to C++, right? – Bartek Banachewicz Mar 10 '13 at 12:57
  • For as much as ownership is concerned, all the user needs is unique_ptr<>. This isn't a situation where the user doesn't know when/where the pointer is released to benefit from shared_ptr<>. So I'm between 2 alternatives, either use free functions (C-like) loosing the nice interface of C++ (with methods syntactically "connected" to the obj), or the user has to use references to pointers (Obj&), which is probably what I will do, but I'm paying a cost that I don't need. – el22 Mar 10 '13 at 13:23
  • @el22: I think I misunderstood you, you removed the `destory` method in the C++ version so I _assumed_ that you meant the destructor to do whatever the destroy method did. In this case there is very much a difference between the C++ `Obj` "handle" class and the old C `Obj*`. In the latter case the client can copy at will (pass by value to functions, etc.) but must explicitly destroy; in the former case the "handle" class must either be non-copyable or must track copies if the "destroy" action must be called only once per handled object. – CB Bailey Mar 10 '13 at 14:02
-1

I assume there's a reason why you need the a pointer to the object in the first place. Because the simplest, and most efficient approach, would be to just create it on the stack:

{
  Obj x(...);
  x.do_some_thing();
  x.do_another_thing();
} // let the destructor handle `destroy()` when the object goes out of scope

But say you need it created on the heap, for whatever reason, most of it is just as simple, and just as efficient:

{
  std::unique_ptr<Obj> x = Obj::create(...); // if you want a separate `create` function
  std::unique_ptr<Obj> x = new Obj(...); // Otherwise, a plain constructor will do

  x->do_some_thing();
  x->do_another_thing();
} // as before, let the destructor handle destruction

There is no need for inheritance or interfaces or virtual functions. You're writing C++, not Java. One of the fundamental rules of C++ is "don't pay for what you don't use". C++ is as performant as C by default. You don't have to do anything special to achieve good performance. All you have to do is not use the features which have a performance cost if you don't need them.

You didn't need reference counting in your C version, so why would you use reference counting in the C++ version (with shared_ptr)? You didn't need the functionality that virtual functions provide in the C version (where it would be implemented through function pointers), so why would you make anything virtual in the C++ function?

But C++ lets you tidy up the create/destroy stuff in particular, and that costs you nothing. So use that. Create the object in its constructor, and let its destructor destroy it. And just place the object in an appropriate scope, so it'll go out of scope when you want it destroyed. And it gives you nicer syntax for calling "member" functions, so make use of that too.

On the other hand unique_ptr<> makes Obj non-copyable. This means that the client can't call his own functions that take Obj by value, and Obj is merely a wrapper for a pointer, so essentially he can't pass pointers by value! He could do that in the original version. (passing by reference still doesn't qualify: he's still passing a pointer to a pointer)

The client can take Obj by value if you use move semantics. But if you want the object to be copied, then let it be copied. Use my first example, and create the object on the stack, and just pass it by value when it needs to be passed by value. It it contains any complex resources (such as pointers to allocated memory), then be sure to write an appropriate copy constructor and assignment operator, and then you can create the object (on the stack, or wherever you need it), pass it around as you desire, by value, by reference, by wrapping it in a smart pointer, or by moving it (if you implement the necessary move constructor and move assignment operator).

jalf
  • 243,077
  • 51
  • 345
  • 550
  • 1
    One of the things the OP is trying to do is make the object opaque, presumably so that the implementation can add new members to the object without requiring a recompile of the caller. I think your approach can work, but there are some details about how to make `Obj` be opaque. – Vaughn Cato Mar 10 '13 at 15:36
  • 1
    @jalf: 'One of the fundamental rules of C++ is "don't pay for what you don't use" ' - exactly, but all your approaches imply that I should provide the full declaration of the implementation (say Obj), with it's data members (and headers where they are defined). I didn't need to do that in the C version. Anyway I edited the question with a possible solution. It would be ideal if existed a version of unique_ptr<> that could derive from a base given as template argument (ObjRef in my edit of the question above), to replace Obj (which does what unique_ptr<> offers), but I can write it myself – el22 Mar 10 '13 at 16:30