How to write conversion from Foo<T> to Foo<T const>?

89 Views Asked by At

I'm writing a class inheriting from std::unique_ptr for classes having a clone function:

template <typename T>
class cl_ptr : public std::unique_ptr<T>
{
    public:
        cl_ptr() noexcept = default;
        cl_ptr(T* p) noexcept : std::unique_ptr<T>(p) {}
        cl_ptr(cl_ptr<T> const& cp) : std::unique_ptr<T>(cp ? cp->clone() : nullptr) {}
        cl_ptr(cl_ptr<T>&&) noexcept = default;
        cl_ptr<T>& operator=(cl_ptr<T> const& cp) { this->reset(cp ? cp->clone() : nullptr); return *this; }
        cl_ptr<T>& operator=(cl_ptr<T>&& cp) noexcept = default;
        ~cl_ptr() noexcept = default;
};

I have an error when I try to convert from an instance with type T to an instance with type T const:

cl_ptr<Foo> p1(new Foo);
cl_ptr<Foo const> p2 = p1; // <- Compiler error here
// error: conversion from ‘cl_ptr<Foo>’ to non-scalar type ‘cl_ptr<const Foo>’ requested

But I don't know how to implement it.

Of course I don't want that this code compiles:

cl_ptr<Foo const> p1(new Foo);
cl_ptr<Foo> p2 = p1; // <- Always wrong

Minimal reproductible example:

# include <memory>

template <typename T>
class cl_ptr : public std::unique_ptr<T>
{
    public:
        cl_ptr() noexcept = default;
        cl_ptr(T* p) noexcept : std::unique_ptr<T>(p) {}
        cl_ptr(cl_ptr<T> const& cp) : std::unique_ptr<T>(cp ? cp->clone() : nullptr) {}
        cl_ptr(cl_ptr<T>&&) noexcept = default;
        cl_ptr<T>& operator=(cl_ptr<T> const& cp) { this->reset(cp ? cp->clone() : nullptr); return *this; }
        cl_ptr<T>& operator=(cl_ptr<T>&& cp) noexcept = default;
        ~cl_ptr() noexcept = default;
};

class Foo
{
    public:
        Foo() = default;
        Foo(Foo const&) = default;
        ~Foo() noexcept = default;
        Foo* clone() const { return new Foo(*this); }
};

int main()
{
    cl_ptr<Foo> p1(new Foo);
    cl_ptr<Foo const> p2 = p1;
    cl_ptr<Foo> p3 = p2; // must fail

    return 0;
}
2

There are 2 best solutions below

0
Caleth On BEST ANSWER

I'm going to allow any conversion of cl_ptr<T> to cl_ptr<U> where there is a conversion from T * to U *, just like std::unique_ptr. This allows T * to const T * and doesn't allow const T * to T *, but also includes Derived * to Base * etc.

template <typename T>
class cl_ptr : public std::unique_ptr<T>
{
    public:
        cl_ptr() noexcept = default;
        cl_ptr(T* p) noexcept : std::unique_ptr<T>(p) {}
        cl_ptr(cl_ptr<T> const& cp) : std::unique_ptr<T>(cp ? cp->clone() : nullptr) {}
        cl_ptr(cl_ptr<T>&&) noexcept = default;
        cl_ptr<T>& operator=(cl_ptr<T> const& cp) { this->reset(cp ? cp->clone() : nullptr); return *this; }
        cl_ptr<T>& operator=(cl_ptr<T>&& cp) noexcept = default;

        template <typename U, typename  = typename std::enable_if_t<std::is_convertible_v<U*, T*>>>
        cl_ptr(cl_ptr<U> const& cp) : std::unique_ptr<T>(cp ? cp->clone() : nullptr) {}
        template <typename U, typename  = typename std::enable_if_t<std::is_convertible_v<U*, T*>>>
        cl_ptr(cl_ptr<U>&& cp) noexcept : std::unique_ptr<T>(std::move(cp)) {}
        template <typename U, typename  = typename std::enable_if_t<std::is_convertible_v<U*, T*>>>
        cl_ptr<T>& operator=(cl_ptr<U> const& cp) { this->reset(cp ? cp->clone() : nullptr); return *this; }
        template <typename U, typename  = typename std::enable_if_t<std::is_convertible_v<U*, T*>>>
        cl_ptr<T>& operator=(cl_ptr<T>&& cp) noexcept { this->reset(cp.release()); return *this; }

        ~cl_ptr() noexcept = default;
};
1
bitmask On

In general: You cannot. A<B const> and A<B> could be entirely different things because of template specialisation. So in general class template instances with different template argument lists are treated as distinct, unrelated types.

In the case of smart pointers the standard library goes to great lengths to enable such conversions under certain circumstances but these are all custom implementations for the respective class template.

You could do the same for your specific class. But cl_ptr seems like a trivial wrapper for std::unique_ptr so why not use std::unique_ptr directly?