Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass object with derived template to function that accepts object with base template

How do I pass an object with a derived template instantiation to a method accepting those objects with a base template instantiation?

It seems possible, as std::shared_ptr or std::pair seem capable to do it.

E.g.

#pragma once

#include <iostream>
#include <memory>

struct Base {
    virtual void print() = 0;
};

struct Derived : public Base {
    void print() {
        std::cout << "Got it!" << std::endl;
    }
};

void printBase(const std::shared_ptr<Base> &ptr){
    ptr->print();
}

void printBase(const std::pair<Base&, Base&> &pr){
    pr.first.print();
}

template <typename T>
struct Wrap {
    T& t;
};

void printBase(const Wrap<Base> &wrap) {
    wrap.t.print();
}

int main() {
    Derived d;
    std::shared_ptr<Derived> ptr = std::make_shared<Derived>(d);
    printBase(ptr); // works
    std::pair<Derived&, Derived&> pr = {d, d};
    printBase(pr); // works
    Wrap<Derived> w = Wrap<Derived>{d};
    // printBase(w); // gives compile error
}
like image 1000
HolKann Avatar asked Jan 29 '26 10:01

HolKann


1 Answers

You will need to explicitly add a conversion constructor and/or assignment operator to your Wrapped type to be able to convert from different types.

This is how both std::shared_ptr and std::pair do this internally; shared_ptr<T> can be constructed from shared_ptr<U> types (with SFINAE restrictions that U* is convertible to T*), and pair<T,U> can be constructed from pair<T2,U2> types (with SFINAE restrictions that T2 is convertible to T and U2 is convertible to U).

Doing this can be as simple as adding a new constructor:

template <typename T>
struct Wrap
{
    Wrap(T& ref)
        : t{ref}
    {
    }

    template <typename U, typename = std::enable_if_t<std::is_convertible_v<U&, T&>>>
    Wrap(const Wrap<U>& other)
        : t{other.t}
    {
    }

    T& t;
};

The above example uses is_convertible as a condition for enable_if so the constructor is only visible when references of U can be converted to references of T. This will constrain it such that U must be hierarchically related to T (since references aren't convertible otherwise) -- which would allow a Wrapped<Derived> to be converted to Wrapped<Base>, but not vice-versa.


Edit: As mentioned in the comments, it's worth noting that, unlike types that are part of a hierarchy -- where a reference to Derived can be passed as a reference to Base, types that wrap hierarchies will not be able to pass a reference to a Template<Derived> as a reference to a Template<Base>.

The examples with a std::shared_ptr<Derived> being passed to a const std::shared_ptr<Base>& only actually work due to const-lifetime extension in C++. This doesn't actually pass it as a reference -- but instead materializes a temporary object of std::shared_ptr<Base> which gets passed to the reference. It's effectively the same as passing-by-value.

This also means that you cannot have a Template<Derived> be passed to a non-const Template<Base> reference, since lifetime extension only occurs for const references.


Edit: As discussed in the comments:

Making the constructor a copy conversion is not required; it could easily be an R-value constructor instead. However, if you are doing cleanup on destruction of the wrapped type, then you will need to somehow flag that the moved-from object does not need to be cleaned up. The easiest way is using pointers, where you rebind to nullptr after moving:

template <typename T>
struct Wrap
{
    Wrap(T& ref)
        : t{std::addressof(ref)}
    {
    }

    Wrap(Wrap&& other) 
        : t{other.t}
    {
        other.t = nullptr;
    }

    Wrap(const Wrap&) = delete;

    template <typename U, typename = std::enable_if_t<std::is_convertible_v<U&, T&>>>
    Wrap(Wrap<U>&& other)
        : t{other.t}
    {
        other.t = nullptr;
    }

    ~Wrap() {
      if (t != nullptr) {
          cleanup(*t); // some cleanup code
      }
    }

    T* t;
};

If pointers aren't possible for your desired API, then you may need to use a bool needs_cleanup that gets set appropriately during moves, since references cannot be rebound.

Note: if the data is private and not public as in this example, you may need to have a friend declaration of:

template <typename> friend class Wrap;

So that a Wrap<T> may access the private data members of a Wrap<U>.

like image 110
Human-Compiler Avatar answered Feb 01 '26 03:02

Human-Compiler



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!