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
}
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>.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With