1

I have a a lot of public interfaces (actually abstract classes with just pure-virtual functions). Only the destructor is marked as default but wouldn't it be more cleaner to delete the copy/move constructors and copy/move assignment operators? Is there actually a guideline for such "interfaces" that one should delete these constructors/assignment operators? Like:

class MyInterface
{
  public:
    virtual ~MyInterface() = default; 
    MyInterface(const MyInterface&) = delete;
    MyInterface(const MyInterface&&) = delete;
    MyInterface& operator=(const MyInterface&) = delete;
    MyInterface& operator=(const MyInterface&&) = delete;
 
    [[nodiscard]] virtual std::string getName() const = 0;
    ...
};
Suslik
  • 929
  • 8
  • 28
  • It is what do, I also make the default constructor protected. And I don't like macros much but for this I made one that will write all that for me. So that an interface looks something like `class MyInterface { IS_INTERFACE(MyInterface); ... }` – Pepijn Kramer Jan 16 '23 at 14:59
  • 4
    That forces every single subclass to implement special member functions in order to use them. I don't see any gain from that, and I never saw any use like that in real code. – Yksisarvinen Jan 16 '23 at 15:00
  • 3
    Seems like a lot of clutter for no good reason. What benefit do you see from doing this? – Pete Becker Jan 16 '23 at 15:02
  • @Yksisarvinen Well In the derived classes I always make an explicit choice whether they are movable/copyable and add those constructors again if needed. In the end a small effort for being explicit – Pepijn Kramer Jan 16 '23 at 15:03
  • 1
    That just ensures that the derived classes can not avoid defining default operations. What is the alleged benefit of that? – Öö Tiib Jan 16 '23 at 15:19
  • 5
    All this does is add hoops for client code to jump through, for no purpose other than jumping through hoops. Anybody can hand write a c'tor that doesn't use the deleted ones, but now they have to address every other member and base. You deny them the rule of zero, and that is counter productive. – StoryTeller - Unslander Monica Jan 16 '23 at 15:20
  • You can't make a variable of type MyInterface because it's abstract, so what are you hoping to prevent? – user253751 Jan 16 '23 at 15:26
  • I was reluctant to VTC as opinion-based, because there could be a technical answer here explaining any potential differences, but seeing as there are now two opinion answers, that seems reasonable course of action. – Yksisarvinen Jan 16 '23 at 15:33
  • I use the ReSharper tool, it also showed me as a clang-tidy check that it would be nicer to define the constructors and assignment operators. I also though it would be better to define the copy constructor for all subclasses at the moment when I need it. But I never saw it in a book/example that somebody does that. So I am confused. – Suslik Jan 16 '23 at 15:37
  • The c++ core guidelines suggests to suppress the copy/move semantics for abstract classes as stated in my edited answer. – Mestkon Jan 16 '23 at 15:40

3 Answers3

3

Copying is about data. Since here there are no data members trying to do anything about copy/move semantics makes no sense.

johnmelas
  • 46
  • 2
1

There is no definite correct answer to this question. It depends on the intended use case of the interface.

As a guideline, the C++ Core Guidelines recommends to explicitly delete copy/move semantics in an inheritance hierarchy to drastically reduce the probability for accidental object slicing. It is hard be confident that the copied object is actually the most derived object. If copy semantics is desired for a polymorphic object, then it is suggested to add a virtual function to clone the object.

template<class T> using owning = T;

class Base
{
    Base(const Base&) = delete;
    Base(Base&&) = delete;
    Base& operator=(const Base&) = delete;
    Base& operator=(Base&&) = delete;

    virtual ~Base() = default;

    virtual owning<Base*> clone() const = 0;
};

class Derived : public Base
{
    owning<Derived*> clone() const override { /* explicit copy */ }
};

// There can be a long chain of derived classes
class Derived2 : public Derived
{
    owning<Derived2*> clone() const override { /* impl */ }
}
Mestkon
  • 3,532
  • 7
  • 18
  • @Yksisarvinen Removed the subjective bias by referring to the core guidelines. There is always slicing danger with polymorphic classes as you cannot control how other people will extend them. – Mestkon Jan 16 '23 at 15:55
  • I did not issue the downvote here if that's why you mention me :) But it still doesn't seem like a good question or good answers to me, so I won't give upvotes either. – Yksisarvinen Jan 16 '23 at 15:59
  • That is fair enough – Mestkon Jan 16 '23 at 16:07
0

Copy/move assignment can cause problems in actual code as follows:

void some_func( MyInterface* lhs, MyInterface* rhs ) {
  *lhs = *rhs;
}

this is an example of slicing, but we are doing a nonsense slice assign.

In theory you can write a polymorphic assignment:

  MyInterface& operator=(MyInterface const& rhs)const&{
    AssignFrom(rhs);
  }
  virtual void AssignFrom(MyInterface const& rhs)const&=0;

but the situations where this makes sense are limited. Here are a few:

  1. This interface exists as a contract for a single fixed implementation. As such, standard assignment semantics are easy to implement.
  2. Assignment semantics are being replaced for this class because we are using the embedded sublanguage technique.

For cases other than this, sensible assignment at the interface level is difficult to justify, as if the two assigned objects disagree on their type, assignment is unlikely to give good semantics.

For the constructor case, no attempt to directly use a constructor of an abstract class can succeed already. Any use of the constructor will be in a context where there is an instance of a child class actually being constructed.

It is plausible that some work needs to be done -- some impurity -- in the interface. For example, if every instance of that interface needs to have its identity logged centrally, implementing constructors makes sense.

This is also a rare case.

In 999/1000 cases, =deleteing them is the correct move. C++'s built-in object polymorphism doesn't play nice with assignment or copy/move construction. This is one of the reasons why people replace its use with versions that do play nice with it, like std::function.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524