24

Note: the following is C++03 code, but we expect a move to C++11 in the next two years, so we must keep that in mind.

I'm writing a guideline (for newbies, among others) about how to write an abstract interface in C++. I've read both articles of Sutter on the subject, searched the internet for examples and answers, and made some tests.

This code must NOT compile!

void foo(SomeInterface & a, SomeInterface & b)
{
   SomeInterface c ;               // must not be default-constructible
   SomeInterface d(a);             // must not be copy-constructible
   a = b ;                         // must not be assignable
}

All the behaviors above find the source of their problem in slicing: The abstract interface (or non-leaf class in the hierarchy) should not be constructible nor copiable/assignable, EVEN if the derived class can be.

0th Solution: the basic interface

class VirtuallyDestructible
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

This solution is plain, and somewhat naive: It fails all our constraints: It can be default-constructed, copy-constructed and copy-assigned (I'm not even sure about move constructors and assignment, but I have still 2 years to figure it out).

  1. We can't declare the destructor pure virtual because we need to keep it inline, and some of our compilers won't digest pure virtual methods with inline empty body.
  2. Yes, the only point of this class is to make the implementers virtually destructible, which is a rare case.
  3. Even if we had an additional virtual pure method (which is the majority of the cases), this class would be still copy-assignable.

So, no...

1st Solution: boost::noncopyable

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

This solution is the best, because it is plain, clear, and C++ (no macros)

The problem is that it still doesn't work for that specific interface because VirtuallyConstructible can still be default-constructed.

  1. We can't declare the destructor pure virtual because we need to keep it inline, and some of our compilers won't digest it.
  2. Yes, the only point of this class is to make the implementers virtually destructible, which is a rare case.

Another problem is that classes implementing the non-copyable interface must then explicitly declare/define the copy constructor and the assignment operator if they need to have those methods (and in our code, we have value classes that can still be accessed by our client through interfaces).

This goes against the Rule of Zero, which is where we want to go: If the default implementation is ok, then we should be able to use it.

2nd Solution: make them protected!

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      // With C++11, these methods would be "= default"
      MyInterface() {}
      MyInterface(const MyInterface & ) {}
      MyInterface & operator = (const MyInterface & ) { return *this ; }
      MyInterface(MyInterface && ) noexcept {}
      MyInterface & operator = (MyInterface && ) noexcept { return *this ; }
} ;

This pattern follows the technical constraints we had (at least in user code): MyInterface cannot be default-constructed, cannot be copy-constructed and cannot be copy-assigned.

Also, it imposes no artificial constraints to implementing classes, which are then free to follow the Rule of Zero, or even declare a few constructors/operators as "= default" in C++11/14 without problem.

Now, this is quite verbose, and an alternative would be using a macro, something like:

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;

The protected must remain outside the macro (because it has no scope).

Correctly "namespaced" (that is, prefixed with the name of your company or product), the macro should be harmless.

And the advantage is that the code is factored in one source, instead of being copy pasted in all interfaces. Should the move-constructor and move-assignment be explicitly disabled the same way in the future, this would be a very light change in the code.

Conclusion

  • Am I paranoid to want the code to be protected against slicing in interfaces? (I believe I'm not, but one never knows...)
  • What is the best solution among the ones above?
  • Is there another, better solution?

Please remember that this is a pattern that will serve as a guideline for newbies (among others), so a solution like: "Each case should have its implementation" is not a viable solution.

Bounty and results

I awarded the bounty to coredump because of the time spent to answer the questions, and the relevance of the answers.

My solution to the problem will probably go to something like that:

class MyInterface
{
   DECLARE_CLASS_AS_INTERFACE(MyInterface) ;

   public :
      // the virtual methods
} ;

... with the following macro:

#define DECLARE_CLASS_AS_INTERFACE(ClassName)                                \
   public :                                                                  \
      virtual ~ClassName() {}                                                \
   protected :                                                               \
      ClassName() {}                                                         \
      ClassName(const ClassName & ) {}                                       \
      ClassName & operator = (const ClassName & ) { return *this ; }         \
      ClassName(ClassName && ) noexcept {}                                   \
      ClassName & operator = (ClassName && ) noexcept { return *this ; }     \
   private :

This is a viable solution for my problem for the following reasons:

  • This class cannot be instantiated (the constructors are protected)
  • This class can be virtually destroyed
  • This class can be inherited without imposing undue constraints on inheriting classes (e.g. the inheriting class could be by default copiable/moveable)
  • The use of the macro means the interface "declaration" is easily recognizable (and searchable), and its code is factored in one place making it easier to modify (a suitably prefixed name will remove undesirable name clashes)

Note that the other answers gave valuable insight. Thank you to all you who gave it a shot.

Note that I guess I can still put another bounty on this question, and I value enlightning answers enough that should I see one, I would open a bounty just to assign it to that answer.

8
  • 5
    Can't you simply use pure virtual functions in the interface? virtual void bar() = 0; for example? That would prevent your interface from being instanciated.
    – Morwenn
    Apr 11, 2014 at 13:34
  • @Morwenn : As said in the question, that would solve 99% of the cases (I aim for 100% if possible). Even if we choose to ignore the missing 1%, it also would not solve the assignment slicing. So, no, this is not a good solution.
    – paercebal
    Apr 11, 2014 at 13:36
  • @Morwenn : Seriously?... :-D ... I first wrote this question on StackOverflow, and then changed my mind just before submitting it. Do you believe I should delete it here, and submit it to SO?
    – paercebal
    Apr 11, 2014 at 13:47
  • If I am right all you need is virtual ~VirtuallyDestructible() = 0 and virtual inheritance of interface classes (with abstract members, only). You might omit that VirtuallyDestructible, likely. Apr 11, 2014 at 13:48
  • 5
    @paercebal: If the compiler chokes on pure virtual classes then it belongs into the trash. A real interface is by definition pure virtual. Apr 11, 2014 at 13:59

4 Answers 4

15

The canonical way to create an interface in C++ is to give it a pure virtual destructor. This ensures that

  • No instances of the interface class itself can be created, because C++ does not allow you to create an instance of an abstract class. This takes care of the not constructible requirements (both default and copy).
  • Calling delete on a pointer to the interface does the right thing: it calls the destructor of the most-derived class for that instance.

just having a pure virtual destructor doesn't prevent assignment on a reference to the interface. If you need that to fail as well, then you must add a protected assignment operator to your interface.

Any C++ compiler should be able to handle a class/interface like this (all in one header file):

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

If you do have a compiler that chokes on this (which means it must be pre-C++98), then your option 2 (having protected constructors) is a good second-best.

Using boost::noncopyable is not advisable for this task, because it sends the message that all classes in the hierarchy should be non-copyable and it can thus create confusion for more experienced developers who wouldn't be familiar with your intentions for using it like this.

2
  • If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface. : This is the root of my problem. The cases where I need an interface to support assignment must be rare indeed. In the other hand, the cases where I want to pass an interface by reference (the cases where NULL is not acceptable), and thus, want to avoid a no-op or slicing that compile are much greater.
    – paercebal
    Apr 21, 2014 at 19:13
  • As the assignment-operator should never be called, why do you give it a definition? As an aside, why not make it private? In addition, you might want to deal with default- and copy-ctor. Aug 2, 2016 at 18:34
5
+300

Am I paranoid...

  • Am I paranoid to want the code to be protected against slicing in interfaces? (I believe I'm not, but one never knows...)

Isn't this a risk management issue?

  • do you fear that a bug related to slicing is likely to be introduced?
  • do you think it can go unnoticed and provoke unrecoverable bugs?
  • to what extent are you willing to go to avoid slicing?

Best solution

  • What is the best solution among the ones above?

Your second solution ("make them protected") looks good, but bear in mind that I am not a C++ expert.
At least, the invalid usages seem to be correctly reported as erroneous by my compiler (g++).

Now, do you need macros? I would say "yes", because even though you don't say what is the purpose of the guideline you are writing, I guess this is to enforce a particular set of best practices in your product's code.

For that purpose, macros can help detect when people effectively apply the pattern: a basic filter of commits can tell you whether the macro was used:

  • if used, then the pattern is likely to be applied, and more importantly, correctly applied (just check that there is a protected keyword),
  • if not used, you can try to investigate why it wasn't.

Without macros, you have to inspect whether the pattern is necessary and well-implemented in all cases.

Better solution

  • Is there another, better solution?

Slicing in C++ is nothing more than a peculiarity of the language. Since your are writing a guideline (esp. for newbies), you should focus on teaching and not just enumerating "coding rules". You have to make sure that you really explain how and why slicing occurs, along with examples and exercices (don't reinvent the wheel, get inspiration from books and tutorials).

For example, the title of an exercise could be "What is the pattern for a safe interface in C++?"

So, your best move would be to ensure that your C++ developpers understand what is going on when slicing occurs. I am convinced that if they do, they won't make as many mistakes in the code as you'd fear, even without formally enforcing that particular pattern (but you can still enforce it, compiler warnings are good).

About the compiler

You say :

I have no power on the choice of compilers for this product,

Often people will say "I don't have the right to do [X]", "I'm not supposed to do [Y]...", ... because they think this is not possible, and not because they tried or asked.

It is probably part of your job description to give your opinion regarding technical issues; if you really think the compiler is the perfect (or unique) choice for your problem domain, then use it. But you also said "pure virtual destructors with inline implementation is not the worst choking point I've seen"; from my understanding, the compiler is so special that even knowledgeable C++ developpers have difficulties using it: your legacy/in-house compiler is now technical debt, and you have the right (the duty?) to discuss that issue with other developpers and managers.

Try to assess the cost of keeping the compiler vs. the cost of using another one:

  1. What does the current compiler brings you that no other one can?
  2. Is your product code easily compilable using another compiler? Why not?

I don't know your situation, and in fact you probably have valid reasons to be tied to a specific compiler.
But in the case this is just plain inertia, the situation won't ever evolve if you or your coworkers don't report productivity or techical debt problems.

5
  • Am I paranoid...: "Make your interfaces easy to use correctly, and hard to use incorrectly". I've tasted that particular principle when someone reported one of my static methods was, by mistake, used incorrectly. The error produced seemed unrelated, and it took multiple hours of an engineer to find the source. This "interface error" is in par with assigning an interface reference to another. So, yes, I want to avoid that kind of error. Also, in C++, the philosophy is to catch as much as possible at compile time, and the language gives us that power, so we go with it.
    – paercebal
    Apr 14, 2014 at 18:26
  • Best solution: I agree. . . Better solution: That's an awesome answer. I'll work on it... Now, about the Pure virtual classes: What's this? A C++ abstract interface? (class without state and only pure virtual methods?). How this "pure virtual class" protected me against slicing? (pure virtual methods will make instantiation not compile, but copy-assignment will, and move assignment will, too IIRC).
    – paercebal
    Apr 14, 2014 at 18:31
  • About the compiler: We agree, but our compilers is outside my scope of responsibility (not that it stops me from snarky comments... :-p ...). I won't disclose the details (I wish I could) but it is tied to internal reasons (like test suites) and external reasons (e.g. client linking with our libraries). In the end, changing the compiler version (or even patching it) is NOT a trivial operation. Let alone replace one broken compiler with a recent gcc.
    – paercebal
    Apr 14, 2014 at 18:38
  • @paercebal thanks for your comments; about pure virtual classes, you are right, it does not solve all your constraints (I will remove this part). I understand the "interface error" part and how catching errors at compile-time is useful: but you asked if you are paranoid, and I think the rational approach is to balance your need for static checks with the likeliness of the mistake happening. Good luck with the compiler thing :)
    – coredump
    Apr 14, 2014 at 19:44
  • 1
    I'm not a fan of the macros, especially because the guidelines are targeted (also) at junior-men. Too often, I have seen people who were given such “handy” tools to apply them blindly and never understand what was actually going on. They come to believe that what the macro does must be the most complicated thing because their boss thought it would be too difficult for them to do themselves. And because the macro only exists in your company, they cannot even do a web search for it whereas for a documented guideline what member functions to declare and why, they could.
    – 5gon12eder
    Oct 10, 2015 at 4:32
2

The problem of slicing is one, but certainly not the only one, introduced when you expose a run-time polymorphic interface to your users. Think of null pointers, memory management, shared data. None of these are easily solved in all cases (smart pointers are great, but even they are no silver bullet). In fact, from your post it doesn't seem like you are trying to solve the problem of slicing, but rather sidestep it by not allowing users to make copies. All you need to do to offer a solution to the slicing problem, is add a virtual clone member function. I think the deeper problem with exposing a run-time polymorphic interface is that you force users to deal with reference semantics, which are harder to reason about than value semantics.

The best way that I know of to avoid these problems in C++ is to use type erasure. This is a technique where you hide a run-time polymorphic interface, behind a normal class interface. This normal class interface then has value semantics and takes care of all the polymorphic 'mess' behind the screens. std::function is a prime example of type erasure.

For a great explanation of why exposing inheritance to your users is bad and how type erasure can help to fix that see these presentations by Sean Parent:

Inheritance Is The Base Class of Evil (short version)

Value Semantics and Concepts-based Polymorphism (long version; easier to follow, but sound is not great)

0

You're not paranoid. My first professional task as a C++ programmer resulted in slicing and crashing. I know of others. There are not a lot of good solutions for this.

Given your compiler constraints, option 2 is the best. Instead of making a macro, which your new programmers will view as weird and mysterious, I would suggest a script or tool for auto generating the code. If your new employees will be using an IDE, you should be able to create a "New MYCOMPANY Interface" tool that will ask for the interface name, and create the structure you are looking for.

If your programmers are using command line, then use whatever scripting language is available to you to create the NewMyCompanyInterface script to generate the code.

I have used this approach in the past for common code patterns (interfaces, state machines, etc). The nice part is that new programmers can read the output and understand it easily, reproducing the needed code when they need something that can't be generated.

Macros and other meta-programming approaches tend to obfuscate what is happening, and the new programmers don't learn what is happening 'behind the curtain'. When they have to break the pattern, they are just as lost as before.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.