Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is rule C.67 too inflexible? #1151

Closed
jwakely opened this issue Mar 7, 2018 · 11 comments
Closed

Is rule C.67 too inflexible? #1151

jwakely opened this issue Mar 7, 2018 · 11 comments
Assignees

Comments

@jwakely
Copy link
Contributor

jwakely commented Mar 7, 2018

C.67 A base class should suppress copying, and provide a virtual clone instead if "copying" is desired

Enforcement
A class with any virtual function should not have a copy constructor or copy assignment operator (compiler-generated or handwritten).

That rule requires deleting all the copy/move ops, but is that rule too strong? Isn't it OK to make them protected, so only derived classes can copy/move the base class?

class B {
public:
  virtual ~B() = default;
  // virtual functions ...
protected:
  B(const B&) = default;
  B& operator=(const B&) = default;
  B(B&&) = default;
  B& operator=(B&&) = default;
};

class D final : public B {
public:
  ~D() = default;
  D(const D& other) : B(other) // ...
  D& operator=(const D& other) // ...
  D(D&& other) noexcept : B(other) // ...
  D(& operator=(D&& other) noexcept // ...
  // ...
};

Or even if the derived type isn't copyable and provides a D::clone() member as per C.67, the base class' copy constructor can still be useful to implement that (if a copy is made inside the D::clone() override).

Alternatively, if the base is an abstract class then it can't be sliced anyway (and also can't define a sensible clone() function so it has to be pure virtual).

Maybe the enforcement could be improved, so that it's OK to have copy/move ops if they are protected, or if the class is abstract.

@blakehawkins
Copy link
Member

It would be nice to have a concrete counterexample to C.67 where a derived class wants to copy for a good reason.

@MikeGitb
Copy link

MikeGitb commented Mar 9, 2018

I certainly want to be able to copy a Circle even if it is derived from a IShape and considering that a circle is probably just 3 doubles (center & radius) I prefer to not use clone for that (which requires a dynamic memory allocation an returns either a raw pointer or a smart_pointer to the base class) if it isn't necessary.

@hsutter
Copy link
Contributor

hsutter commented Mar 9, 2018

Mike, could you elaborate on that example? Starting with IShape:

class IShape { /*...public pure virtual functions...*/ };

Does IShape suppress copying? It's an ABC, so it ought to, right?

Then Circle:

class Circle : public IShape {
    /* circle data */
public:
    Circle(Circle const&) { /*...*/ }
    Circle& operator=(Circle const&) { /*...*/ }
};

How would you write those functions? What would they do?

Then finally:

class BlueCircle : public Circle {
    /* more data */
public:
    /* copying? */ 
};

BlueCircle get_circle();

int main() {
    Circle c = get_circle();
}

Would BlueCircle also be copyable? Is the slicing in main intended?

@MikeGitb
Copy link

MikeGitb commented Mar 10, 2018

@hsutter:

What I said was meant in support of what jwakely suggested: protected copy construction/assignment should be allowed. Now if you really want to do public implementation inheritance you can solve that problem via another level of indirection:

struct IShape{
	virtual void draw() = 0;
	virtual std::unique_ptr<IShape> clone() = 0;
	virtual ~IShape() = default;
protected:
	IShape() = default;
	IShape(const IShape&) = default;
	IShape& operator=(const IShape&) = default;
};

struct CircleImpl : IShape {
	void draw() override { std::cout << "Draw Circle"; };
protected:
	CircleImpl() = default;
	CircleImpl(const CircleImpl&) = default;
	CircleImpl& operator=(const CircleImpl&) = default;
};

struct Circle final : CircleImpl {
	Circle() = default;
	Circle(const Circle&) = default;
	Circle& operator=(const Circle&) = default;

	std::unique_ptr<IShape> clone() override { return std::make_unique<Circle>(*this); }
};

struct BlueCircleImpl : CircleImpl {
	void draw() override { std::cout << "Draw BlueCircle"; }
protected:
	BlueCircleImpl() = default;
	BlueCircleImpl(const BlueCircleImpl&) = default;
	BlueCircleImpl& operator=(const BlueCircleImpl&) = default;
};

struct BlueCircle final : BlueCircleImpl {
	BlueCircle() = default;
	BlueCircle(const BlueCircle&) = default;
	BlueCircle& operator=(const BlueCircle&) = default;

	std::unique_ptr<IShape> clone() override { return std::make_unique<BlueCircle>(*this); }
};

BlueCircle get_circle() { return BlueCircle{}; }
std::unique_ptr<IShape>  circle_factory(bool colored) {
	if (colored) {
		return std::make_unique<BlueCircle>();
	} else {
		return std::make_unique<Circle>();
	}
}

int main() {
	std::unique_ptr<IShape> s = circle_factory(true);
	auto s2 = s->clone();

            auto c = get_circle();
            auto c2 = c;

	Circle c = get_circle();  // compiler error -> no accidential slicing
}

My general rule of thumb is: Don't inherit publicly from classes with public constructors or assignment operators and vice versa, mark any class with public constructors final.

Regardingthe polymorphic types, I think it is important to realize two distinct cases: In the one case, the object is created by some factory on the heap and only ever accessed through the base class interface. In that case, there is obviously no real need for copy constructors and such .

The other case however is, where the object is treated just like an normal object in one part of the program (created on the stack, copied around, accessed through expressions of the "correct" dynamic type), but is then presented through a polymorphic interface to another part

@hsutter
Copy link
Contributor

hsutter commented Apr 9, 2018

Back to Jonathan's original question: Should protected copying be allowed for a base class?

We think the answer is no: Since there will already be a clone function, the derived implementation can already access that and any copying would be in addition and duplicative.

Also, as a general principle we are trying to make the guidelines restrict what is possible in the language rather than support every coding style.

Does that make sense?

@MikeGitb
Copy link

MikeGitb commented Apr 9, 2018

@hsutter:

How does the base classe's clone function solve that problem? The base version's clone function will create a dynamically allocated base class object. How can you turn that into a derived object after the fact? Could you demonstrate how you would do it?

Even worse: If my base class is an abstract class, it can't even implement clone in the first place.

I certainly don't have knowledge about a representative c++ code base with millions and millions of lines of code, but my feeling is that what you call "[not] support every coding style" effectively outlaws a lot of established practice (of course some people consider dynamic polymorphism the work of the devil so restricting it might be exactly what you want).

@hsutter
Copy link
Contributor

hsutter commented Apr 16, 2018

Update from 4/16 editors call: @MikeGitb That's a good point. We will think more about this question.

@MikeGitb
Copy link

Any update on this?

@justanotheranonymoususer
Copy link
Contributor

Stumbled upon this as well. See also:
https://stackoverflow.com/questions/46918749/how-to-clone-an-object-without-copy-constructor

@hsutter
Copy link
Contributor

hsutter commented Jun 17, 2021

Editors call: Sorry for the lag -- we agree with Jonathan and have updated C.67 and C.130 to say that base class copy constructors should be nonpublic. They can be =deleted for base classes that have no data (derived copy operations can invoke the base default constructor) or made protected for base classes that do have data (for completeness, but other rules say to avoid those).

@hsutter hsutter closed this as completed Jun 17, 2021
@adah1972
Copy link
Contributor

@hsutter

I am also wondering whether there should be an ‘exception’ regarding abstract classes. If a class has pure virtual functions, is it still necessary to declare copy operations at all? Technically, it is not necessary, as slicing cannot happen.

I.e., I prefer

class Serializable {
public:
    virtual ~Serializable() = default;
    virtual void Serialize(vector<byte>& buffer) = 0;
};

to

class Serializable {
public:
    virtual ~Serializable() = default;
    virtual void Serialize(vector<byte>& buffer) = 0;

protected:
    Serializable(const Serializable&) = default;
    Serializable& operator=(const Serializable&) = default;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants