1

How to avoid unnecessary copying in the following scenario? Class A contains base-type pointer to big object.

class A{
  BigBaseClass *ptr;
  A(const BigBaseClass& ob);
  ~A(){delete ptr;}
};

Sometimes I will need object ob to be copied. So I implement virtual cloning:

class BigBaseClass{
   virtual BigBaseClass* clone() {return new BigBaseClass(*this);}
};
class BigDerivedClass : BigBaseClass{
  virtual BigDerivedClass* clone() {return new BigDerivedClass(*this);}
};
A::A(const BigBaseClass& ob):ptr(ob.clone(){}

But sometimes I will create temporary BigDerivedClass object and use it to construct class A:

A a{BigDerivedClass()};

or

BigDerivedClass f(){
     BigDerivedClass b;
     /*constructing object*/
     return b;
   }
   A a{f()};

Here there is no need to copy created object and then delete it. It's possible to create this object directly in the heap and store its address in a.ptr.

But it seems unlikely to me that compiler is smart enough to implement copy elision here (or is it?). So what would you suggest to avoid such unnecessary copying?

Dmitry J
  • 867
  • 7
  • 20
  • Does that even compile? Your clone's should be returning dynamically allocated objects, and you should initialize with a dynamically allocated object. Also, look into std::unique_ptr to avoid working with bare pointers. – evan Jul 03 '16 at 22:04
  • Oh,sorry, I forgot about "new" in clone() functions. Fixed. – Dmitry J Jul 03 '16 at 22:11
  • 1
    Isn't this what move semantics were invented for? – Lightness Races in Orbit Jul 03 '16 at 22:12
  • And if you replace the constructor with one that takes a BigBaseClass* you can just assign it to ptr, forcing you to allocate it on the heap. That is how you "create objects directly on the heap." – evan Jul 03 '16 at 22:13
  • But sometimes I need to copy object, not move it. As for move semantics, I don't quite understand how to implement it here. – Dmitry J Jul 03 '16 at 22:20
  • 1
    `A a(BigDerivedClass())` [declares function, not object](https://en.wikipedia.org/wiki/Most_vexing_parse) – PcAF Jul 03 '16 at 22:25
  • **PcAf**, are you sure? Would not function be declared as ' A a(BigDerivedClass)'? – Dmitry J Jul 03 '16 at 22:31
  • @Dmitry: PcAF is correct: `A a(BigDerivedClass())` declares a function. You can make it an object, e.g., using `A a{BigDerivedClass()}`. – Dietmar Kühl Jul 03 '16 at 22:38
  • I see. In my code it worked only because I used it with parameters. Fixed. – Dmitry J Jul 03 '16 at 22:49

2 Answers2

4

The compiler will not elide the construction of a copy via clone(): copy elision is only allowed in very specific situations. In all cases where the compiler is allowed to do copy elision the life-times of the objects involved are entirely controlled by the compiler. The four situations are (for details see 12.8 [class.copy] paragraph 8):

  1. Returning a local name by value.
  2. Throwing a local object.
  3. Copying a temporary object which isn't bound to a reference.
  4. When catching by value.

The details when copy-elision is applicable even in these situations are somewhat non-trivial. In any case, return new T(*this); doesn't fit any of these situations.

Typical big objects don't hold their data as part of the object. Instead, they typically hold some data structures which can be moved. If you want to retain the simplicity when using A{f()} without wanting to copy the result of f(), you can get away with a move constructor calling a virtual function transferring the content instead of copying it:

#include <utility>

class BigBaseClass {
public:
    virtual ~BigBaseClass() {}
    virtual BigBaseClass* clone() const = 0;
    virtual BigBaseClass* transfer() && = 0;
};
class A{
    BigBaseClass *ptr;
public:
    A(BigBaseClass&& obj): ptr(std::move(obj).transfer()) {}
    A(BigBaseClass const& obj): ptr(obj.clone()) {}
    ~A(){delete ptr;}
};
class BigDerivedClass
    : public BigBaseClass {
    BigDerivedClass(BigDerivedClass const&); // copy the content
    BigDerivedClass(BigDerivedClass&&);      // transfer the content
    BigDerivedClass* clone() const { return new BigDerivedClass(*this); }
    BigDerivedClass* transfer() && { return new BigDerivedClass(std::move(*this)); }
};

BigDerivedClass f() {
    return BigDerivedClass();
}

int main()
{
    A a{f()};
}

Whether move construction does help copying the big objects does depend on how the objects are internally implemented. If they object essentially just contains a couple of pointers to the actual large data, move construction should avoid any relevant cost as transferring the pointers would be negligible compared to setting up the actual data. If the data is actually held within the object the transfer wouldn't really help (although it is generally a bad idea to so anyway for a variety of reasons).

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • *"although it is generally a bad idea to so anyway for a variety of reasons"* Could you name a few or provide a link to some material regarding this topic? – MikeMB Jul 04 '16 at 06:36
  • @MikeMB: Here are some: stack space is comparatively limited large making colocated objects problematic; moving large objects is undesirable; classes with a large footprint tend to be hard to test; large objects getting their size from array tend to impose arbitrary limits. In some cases it may be acceptable to have objects with a large immediate size but in most cases I have seen things were better off having a small handle to the actual representation. – Dietmar Kühl Jul 04 '16 at 07:24
  • Thanks for the List. I completely agree with the point about arrays, but otherwise I'm somewhat conflicted here: I tend to shift the decision of where the data should live to the usage side: If it should live on the heap, wrap it in a smart pointer. That often makes the class simpler and (slightly) more efficient, but of course complicates the campsite. I don't really get the part about testability though - how is that connected to the memory footprint of your class? – MikeMB Jul 04 '16 at 09:00
  • @MimeMB: re testablity: classes with a large footprint typically consist of an array of elements or of many different components. In the latter case the class is likely fairly complicated and unlikely to be particular testable. If the class has a small footprint chances are that it isn't as complicated and consists of parts which can be tested individually. There are exceptions, of course, and things can compose out if simple classes and still be big. These tend to be rare, though. – Dietmar Kühl Jul 04 '16 at 13:14
0
class BigBaseClass
{
public:
    virtual ~BigBaseClass() {}

    virtual BigBaseClass* clone() const { return new BigBaseClass(*this); }
};

class BigDerivedClass : public BigBaseClass
{
public:
    BigDerivedClass* clone() const override { return new BigDerivedClass(*this); }
};

class A
{
    BigBaseClass *ptr;

public:
    explicit A(BigBaseClass* ob);
    ~A() { delete ptr; }
};

A::A(BigBaseClass* ob) : ptr(ob)
{
}

int main()
{
    A a(new BigDerivedClass);
}

You might think that move semantics would be a good idea, but that doesn't really work in this case since BigBaseClass is a base class, and moving a BigDerivedClass into a BigBaseClass would only move the BigBaseClass parts. But using a smart pointer would be good idea too, unless you are sure that the rest of your code is exception-free.

evan
  • 1,463
  • 1
  • 10
  • 13
  • Do you mean using two different constructor - one taking const reference for copying object, and another one taking pointer for moving? Or you mean that I can make copying with A a(BigDerivedClass_object.clone())? – Dmitry J Jul 03 '16 at 22:54
  • The latter. It is likely not very safe to take the address of an object created on the stack, and store it in an A, unless you know, for certain, that the stack allocated object will outlive the A. It's better to make it obvious what is going on. – evan Jul 03 '16 at 22:59
  • Well, it seems that it won't help in case when BigClass_object is returned by value from a function, but maybe it just means that I should avoid returning big objects by value. – Dmitry J Jul 03 '16 at 23:12
  • An object returned by value shouldn't be put in an A, because it lives on the stack and will be destructed automatically when it goes out of scope. If you want A to handle instances of both BigBaseClass & BigDerivedClass, then the only way to do it is on the heap, and that means that the objects need to be newed into existence. But you could do A(BigDerivedClass_object_returned_by_value.clone()); – evan Jul 03 '16 at 23:28