Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get underlying type of any C++ dereferencable type

I have a function that creates a new object of the underlying type of P. Here P is a dereferencable type like a pointer or a smart pointer.

template<typename P>
auto make_new()

For example, for pointers and smart pointers,

struct A
{
    int a = 3;
};

A* a = make_new<A*>();
std::cout << a->a << std::endl;
delete a;

std::shared_ptr<A> b = make_new<std::shared_ptr<A>>();
std::cout << b->a << std::endl;

Now, for shared pointers, I would implement make_new as the following,

template<typename P>
auto make_new()
{
    using Ptype = typename P::element_type;
    return P(new Ptype);
}

which doesn't work for pointers.

Now, something that works for both pointers and smart pointers,

template<typename P>
auto make_new()
{
    using Ptype = typename std::remove_reference<decltype(*P())>::type;
    return P(new Ptype);
}

but doesn't work for std::optional.

Is there a canonical way of getting the underlying type of a de-referencable object?

I know that * and -> can be overloaded to anything and there is no guarantee that constructor works like above, or makes sense to do.

Just want to know if there is a way and am not just finding it, or just doing something dumb.

like image 607
Mochan Avatar asked May 14 '19 17:05

Mochan


1 Answers

Resolving the element type on both pointers and classes

Target. Our goal is to write a using template which takes a dereference-able type as input, and returns the element type.

template<class T>
using element_type_t = /* stuff */;

Method. We can use SFINAE to check if there's an element_type property, and if there's not, we fall back to using std::remove_reference<decltype(*P())>().

// This is fine to use in an unevaluated context
template<class T>
T& reference_to(); 

// This one is the preferred one
template<class Container>
auto element_type(int) 
  -> typename Container::element_type;

// This one is the fallback if the preferred one doesn't work
template<class Container>
auto element_type(short) 
  -> typename std::remove_reference<decltype(*reference_to<Container>())>::type;

Once we have this function, we can write element_type_t by just getting the return type of element_type.

// We alias the return type
template<class T>
using element_type_t = decltype(element_type<T>(0)); 

Why can't we always get the element_type by dereferencing it? If you try to always get the value type using the * operator, that could cause issues with things like the iterator for std::vector<bool>, which returns an object that acts like a bool, but encapsulates bit manipulations. In these cases, the element type is different than the type returned by dereferencing it.

Determining if the constructor takes a pointer or a value

The reason your code fails with std::optional is because std::optional's constructor takes the value itself, rather than a pointer to the value. In order to determine which constructor we need, we use SFINAE again to make the determination.

// Base case - use new operator
template<class Container>
auto make_new_impl(int) 
    -> decltype(Container{new element_type_t<Container>})
{
    return Container{new element_type_t<Container>};
}

// Fallback case, where Container takes a value
template<class Container>
auto make_new_impl(long)
    -> decltype(Container{element_type_t<Container>()})
{
    return Container{element_type_t<Container>()};
}

Now, we can write make_new so that it calls make_new_impl:

template<class Container>
auto make_new() {
    return make_new_impl<Container>(0);
}

Example. We can now use make_new to make either std::optional, std::shared_ptr, or even a regular pointer.

#include <optional>
#include <memory>

int main() {
    // This works
    int* ptr = make_new<int*>(); 

    // This works too
    std::shared_ptr<int> s = make_new<std::shared_ptr<int>>();

    // This also works
    std::optional<int> o = make_new<std::optional<int>>(); 
}
like image 65
Alecto Irene Perez Avatar answered Oct 22 '22 08:10

Alecto Irene Perez