Can you extend the standard library by inheritance?
Asked Answered
M

10

59

It is a commonly held belief that the the C++ standard library is not generally intended to be extended using inheritance. Certainly, I (and others) have criticised people who suggest deriving from classes such as std::vector. However, this question: Can what() return NULL for exceptions? made me realise that there is at least one part of the Standard Library that is intended to be so extended - std::exception.

So, my question has two parts:

  1. Are there any other standard library classes which are intended to be derived from?

  2. If one does derive from a standard library class such as std::exception, is one bound by the interface described in the ISO Standard? For example, would a program which used an exception class who's what() member function did not return a NTBS (say it returned a null pointer) be standard conforming?

Mattheus answered 2/7, 2009 at 11:58 Comment(0)
E
45

Good nice question. I really wish that the Standard was a little more explicit about what the intended usage is. Maybe there should be a C++ Rationale document that sits alongside the language standard. In any case, here is the approach that I use:

(a) I'm not aware of the existence of any such list. Instead, I use the following list to determine whether a Standard Library type is likely to be designed to be inherited from:

  • If it doesn't have any virtual methods, then you shouldn't be using it as a base. This rules out std::vector and the like.
  • If it does have virtual methods, then it is a candidate for usage as a base class.
  • If there are lots of friend statements floating around, then steer clear since there is probably an encapsulation problem.
  • If it is a template, then look closer before you inherit from it since you can probably customize it with specializations instead.
  • The presence of policy-based mechanism (e.g., std::char_traits) is a pretty good clue that you shouldn't be using it as a base.

Unfortunately I don't know of a nice comprehensive or black and white list. I usually go by gut feel.

(b) I would apply LSP here. If someone calls what() on your exception, then it's observable behavior should match that of std::exception. I don't think that it is really a standards conformance issue as much as a correctness issue. The Standard doesn't require that subclasses are substitutable for base classes. It is really just a "best practice".

Erosion answered 2/7, 2009 at 12:15 Comment(2)
You mentioned not inheriting from classes which don't have virtual members – but then you also mentioned policy classes. So (for example) what's wrong with inheriting from an allocator (privately)?Selfdrive
I would add that some things are meant to have a small degree of extension through inheritance, such as std::stack and std::queue because they have protected, since the only reason for things to be protected is to allow a child class to read the data. Obviously you'd need to be very careful about what you do with this though.Fatal
D
18

a) the stream library is made to be inherited :)

Davisdavison answered 2/7, 2009 at 12:5 Comment(0)
P
8

Regarding your part b, from 17.3.1.2 "Requirements", paragraph 1:

The library can be extended by a C++ program. Each clause, as applicable, describes the requirements that such extensions must meet. Such extensions are generally one of the following:

  • Template arguments
  • Derived classes
  • Containers, iterators, and/or algorithms that meet an interface convention

While 17.3 is informative instead of binding, the committee's intent on derived class behavior is clear.

For other very similar points of extension, there are clear requirements:

  • 17.1.15 "required behavior" covers replacement (operator new, etc.) and handler functions (terminate handlers, etc.) and throws all non-compliant behavior into UB-land.
  • 17.4.3.6/1: "In certain cases (replacement functions, handler functions, operations on types used to instantiate standard library template components), the C++ Standard library depends on components supplied by a C++ program. If these components do not meet their requirements, the Standard places no requirements on the implementation."

In the last point it's not clear to me that the parenthetical list is exhaustive, but given how specifically each mentioned case is dealt with in the next paragraph, it would be a stretch to say the current text was intended to cover derived classes. Additionally, that 17.4.3.6/1 text is unchanged in the 2008 draft (where it's in 17.6.4.8) and I see no issues addressing either it or derived classes' virtual methods.

Premonish answered 17/1, 2010 at 1:22 Comment(0)
P
6

The parsimonious rule is "Any class may be be used as a base class; the resposibility for using it safely in the absence of virtual methods, including a virtual destructor, is entirely the deriving author's." Adding a non-POD member in a child of std::exception is the same user-error as it would be in a derived class of std::vector. The idea that the containers are not "intended" to be base classes is an engineering example of what the literature professors call The Fallacy of Authorial Intent.

The IS-A principle dominates. Do not derive D from B unless D can substitute for B in every respect in B's public interface, including the delete operation on a B pointer. If B has virtual methods, this restriction is less onerous; but if B has only nonvirtual methods, it is still both possible and legitimate to specialize with inheritance.

C++ is multiparadigmatic. The template library uses inheritance, even inheritance from classes with no virtual destructors, and thus demonstrates by example that such constructs are safe and useful; whether they were intended is a psychological question.

Parceling answered 3/7, 2009 at 22:4 Comment(0)
M
5

The C++ standard library isn't a single unit. It is the result of combining and adopting several different libraries (a large chunk of the C standard library, the iostreams library and the STL are the three main building blocks, and each of these have been specified independently)

The STL part of the library is generally not meant to be derived from, as you know. It uses generic programming, and generally avoids OOP.

The IOStreams library is much more traditional OOP, and uses inheritance and dynamic polymorphism heavily internally --- and users are expected to use the same mechanisms to extend it. Custom streams are typically written by deriving from either the stream class itself, or the streambuf class it uses internally. Both of these have virtual methods that can be overridden in derived classes.

std::exception is another example.

And like D.Shawley said, I would apply the LSP to your second question. It should always be legal to substitute the base class for a derived one. If I call exception::what(), it must follow the contract specified by the exception class, no matter where the exception object came from, or whether it is actually a derived class having been upcasted. And in this case, that contract is the standard's promise of returning a NTBS. If you made a derived class behave differently, then you'd violate the standard because an object of type std::exception no longer returns a NTBS.

Megavolt answered 2/7, 2009 at 12:24 Comment(0)
S
4

To answer question 2):

I believe that yes, they would be bound by the interface description of the ISO standard. For example, the standard allows redefining operator new and operator delete globally. However, the standard guarantees that operator delete is a no-operation on null pointers.

Not respecting this is certainly undefined behaviour (at least to Scott Myers). I think we can say that the same is true by analogy for other areas of the standard library.

Selfdrive answered 2/7, 2009 at 12:11 Comment(0)
D
4

Some of the stuff in functional, like greater<>, less<>, and mem_fun_t are derived from unary_operator<> and binary_operator<>. But, IIRC, that only gives you some typedefs.

Dwarfish answered 2/7, 2009 at 12:11 Comment(0)
R
3

For the second question I believe the answer is yes. The standard says that the what member of std::exception must return a non-NULL value. It shouldn't matter if I have a stack, reference or pointer value to std::exception. The return of what() is bound by the standard.

Of course it is possible to return NULL. But I would consider such a class to be non-standards conforming.

Respite answered 2/7, 2009 at 12:11 Comment(0)
S
3

I know this question is old, but I'd like to add my comment here.

Since some years now, I use a class CfgValue which inherits from std::string, though the documentation (or some book or some standard doc, I do not have the source handy now) says, that users should not inherit from std::string. And this class contains a "TODO: remove inheritance from std::string" comment since years.

Class CfgValue just adds some constructors and setters and getters to quickly convert between strings and numeric and boolean values, and conversion from utf8 (kept in std::string) to ucs2 (kept in std::wstring) encoding and so on.

I know, there are many different ways to do this, but it is just extremely handy for the user, and it works fine (which means, it does not break any stdlib concepts, exception handling and the like):

class CfgValue : public std::string {
public:
    ...
    CfgValue( const int i ) : std::string() { SetInteger(i); }
    ...
    void SetInteger( int i );
    ...
    int GetInteger() const;
    ...
    operator std::wstring() { return utf8_to_ucs16(*this); }
    operator std::wstring() const { return utf8_to_ucs16(*this); }
    ...
};
Soemba answered 13/12, 2009 at 23:9 Comment(0)
P
1

w.r.t question 2), as per C++ standard, the derived exception-class must specify a no-throw i.e. throw() specification as well along with returning non-null. This means in many cases that the derived exception class should not use std::string, as std::string itself might throw depending on the implementation.

Pentose answered 2/7, 2009 at 12:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.