The Standard Library classes (such as std::string
and std::vector
) are not usually regarded as being able to be derived from. They are little black boxes whose functionality is clearly specified in the C++ Standard and which should not be redefined. However, let us put this to one side for a moment and consider that it should be possible, if not always desirable.
Say we want to add two new member functions to std::string
, being utf8at()
(which takes a index and returns a char32_t
), and an overload to append()
(which takes a char32_t
). There are a number of ways of doing this:
- Composition: Our new class (String) has a member of type
std::string
, which is modified by String’s member functions. This likely has a performance overhead, and involves a lot of effort to create the class. - Private Inheritance: Our new String is derived from
std::string
, but using private, not public, inheritance. None of the member functions ofstd::string
are accessible from outside String, so forwarding functions (again) have to be written. This is almost as much work as (1). - Non-member functions: We could forget about writing a whole new class and simply implement the two functions as free functions. However this is ugly and not best suited to Modern C++.
- Public Inheritance: All of
std::string
‘s member functions magically become available to clients of String. The only thing to remember is to not use a base class pointer under any circumstances (that is, reference aString*
from astd::string*
pointer, which would cause memory leaks upondelete
). Let’s look at this option in more detail:
The following code compiles without any issues:
#include <string>
#include <iostream>
class String : public std::string {
};
int main() {
String s;
s.append("hello");
std::cout << s << std::endl;
}
“Great!” you think, and get on with writing a program which uses String. However there are a couple of issues to solve. Firstly code like String s{ 8, '*' };
fails to compile with an error such as: “constructor overload resolution was ambiguous”. Secondly, the return type of member functions such as append()
and substr()
is still std::string
, not String.
To solve the first of these issues using Modern C++, we can apply perfect forwarding to a generic constructor, which avoids us having to write out all of the possibilities manually. This can be achieved as shown here:
class String : public std::string {
public:
template<typename... Args>
explicit String(Args&&... args)
: std::string(std::forward<Args>(args)...) {}
};
With perfect forwarding, String’s clients can enjoy access to all of std::string
‘s constructors. Note the use of explicit
, which ensured that any (possibly undesirable) automatic type conversions cannot take place; code which fails to compile is better than silently incorrect code. Also, note that perfect forwarding is guaranteed to not modify the parameters and should generate as efficient code as using the target function directly.
The second issue can be solved in almost the same way. The only problem with substr()
is that its return type is std::string
, not String; this is corrected using a safe static_cast<String>
. The parameters do not need to be copied as we can use perfect forwarding again, as shown here:
class String : public std::string {
public:
// ...
template<typename... Args>
String substr(Args&&... args) {
return static_cast<String>(std::string::substr(std::forward<Args>(args)...));
}
};
This code is simply boilerplate which can be applied to other member functions of std::string
which return *this
(a macro could also be used to accomplish this).
So there you have it, perfect forwarding to base class constructors and member functions, without much effort, with no performance penalty and with full type safety. The only pain is the error messages which can be associated with such variadic-template-using functions.