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:

  1. Both the base and derived return types are pointers or references to classes.
  2. The class pointed to or referenced by the derived function is an accessible, unambiguous derived class of the one returned by the base function.
  3. 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 from std::unique_ptr<Base>.
  • std::shared_ptr<Derived> does not inherit from std::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.