Covariant Return Types with Smart Pointers in C++
This article examines why C++ does not support covariant return types
when using smart pointer templates such as std::unique_ptr
and std::shared_ptr. It explains the core language
specifications restricting covariance strictly to native pointers and
references, analyzes the compiler failures triggered by smart pointer
templates, and outlines standard workarounds used to circumvent these
limitations.
Understanding Covariant Returns in C++
In standard C++, a virtual function in a derived class can override a base class member function and return a different type, provided that the return type is "covariant." According to the C++ standard, a return type is covariant if:
- Both the base and derived return types are pointers or references to classes.
- The class pointed to or referenced by the derived function is an accessible, unambiguous derived class of the one returned by the base function.
- The cv-qualification of the derived return type is identical to or less than that of the base return type.
For example, overriding a function returning Base* with
a function returning Derived* is valid and supported
natively by the compiler.
Why Smart Pointer Templates Fail Covariance
Smart pointers such as std::unique_ptr<T> and
std::shared_ptr<T> are class templates, not built-in
pointer types. When instantiated with different types, they produce
entirely distinct, unrelated classes in the C++ type system:
std::unique_ptr<Derived>does not inherit fromstd::unique_ptr<Base>.std::shared_ptr<Derived>does not inherit fromstd::shared_ptr<Base>.
Even though std::unique_ptr<Derived> provides an
implicit converting constructor or conversion operator to
std::unique_ptr<Base>, this conversion involves
constructing a new object rather than expressing a subtyping
relationship.
Because covariance at the language level relies on the runtime
ability to adjust raw memory offsets (specifically the this
pointer adjustment via the vtable) between derived and base class
layouts, the compiler cannot perform covariance on arbitrary wrapper
classes. The C++ standard explicitly limits covariant returns to raw
pointers (T*) and raw references (T&).
Compilation Errors
Attempting to compile an override using smart pointers with differing template arguments results in an immediate compilation failure:
#include <memory>
class Base {
public:
virtual ~Base() = default;
virtual std::unique_ptr<Base> clone() const {
return std::make_unique<Base>(*this);
}
};
class Derived : public Base {
public:
// Compilation Error: Invalid covariant return type
std::unique_ptr<Derived> clone() const override {
return std::make_unique<Derived>(*this);
}
};Common compiler outputs for this code include:
- GCC / Clang:
error: invalid covariant return type for 'virtual std::unique_ptr<Derived> Derived::clone() const' - MSVC:
error C2555: 'Derived::clone': overriding virtual function return type differs and is not covariant from 'Base::clone'
The compiler treats std::unique_ptr<Derived> and
std::unique_ptr<Base> simply as two mismatched return
types on a virtual override.
Standard Solutions
To preserve type safety and ownership semantics without violating language rules, two primary patterns are employed.
1. The Non-Virtual Interface (NVI) Pattern
The NVI pattern separates the virtual dispatch mechanism from the public interface. The virtual method returns a raw pointer using built-in covariance, while public non-virtual methods wrap the result in the appropriate smart pointer:
#include <memory>
class Base {
public:
virtual ~Base() = default;
std::unique_ptr<Base> clone() const {
return std::unique_ptr<Base>(clone_impl());
}
protected:
virtual Base* clone_impl() const {
return new Base(*this);
}
};
class Derived : public Base {
public:
std::unique_ptr<Derived> clone() const {
return std::unique_ptr<Derived>(clone_impl());
}
protected:
Derived* clone_impl() const override {
return new Derived(*this);
}
};2. Returning the Base Smart Pointer Uniformly
If exact-type return values are not strictly required at the derived call site, derived classes can override the function by returning the base smart pointer type containing the derived instance:
class Derived : public Base {
public:
std::unique_ptr<Base> clone() const override {
return std::make_unique<Derived>(*this);
}
};This approach leverages the converting constructor of
std::unique_ptr<Base> from
std::unique_ptr<Derived>, satisfying the requirement
that the overriding signature matches the base virtual function
signature exactly.