I don't know why there's no such function in std (at least I didn't find one). I need a conditional template function to return a reference to an object of T
. T
might be passed as a pointer or a smart pointer or by reference:
template <typename T>
void do_foo(T &t)
{
auto &&res = dereference(t).foo();
}
So I wrote this:
#include <memory>
#include <type_traits>
namespace eld::traits
{
template<typename T>
struct is_pointer_like : std::is_pointer<T>
{
using value_type = T;
};
template<typename T, typename DeleterT>
struct is_pointer_like<std::unique_ptr<T, DeleterT>> : std::true_type
{
using value_type = T;
};
template<typename T>
struct is_pointer_like<std::shared_ptr<T>> : std::true_type
{
using value_type = T;
};
namespace detail
{
template<typename T, bool /*false*/ = is_pointer_like<T>::value>
struct dereference
{
constexpr explicit dereference(T &t) //
: value(t)
{
}
T &value;
};
template<typename PtrT>
struct dereference<PtrT, true>
{
constexpr explicit dereference(PtrT &ptrT)
: value(*ptrT)
{}
typename is_pointer_like<PtrT>::value_type &value;
};
} // namespace detail
template <typename T>
constexpr auto& dereference(T &t)
{
return detail::dereference<std::decay_t<T>>(t).value;
}
template <typename T>
constexpr const auto& dereference(const T &t)
{
return detail::dereference<const std::decay_t<T>>(t).value;
}
} // namespace eld::traits
This code does not support the concept of shared pointer though. dereference
does not increment a counter, so using an obtained reference is dangerous. Because when std::shared_ptr
is used the user is expected to hold a copy of it (like with std::weak_ptr
).
Taking it into an account I suppose using dereference
as a function is a wrong approach. I think that dereference
should be a persistent object if I expect it to support a concept of sharable ownership.
template <typename T>
void so_stuff(T &t)
{
dereference derefT{t};
auto &refT = derefT.value();
auto &&res = refT.foo();
}
However, this becomes convoluted in use.
Also, I feel like using is_pointer_like
is redundant. Maybe I am better of just implementing dereference
with SFINAE customizations for raw and smart pointers.
1 Answer 1
It is probably a bad idea
Your dereference()
will even work if you don't give it a pointer-like type:
int foo = 42;
std::cout << eld::traits::dereference(foo) << '\n';
That prints out 42. Maybe it's still simple when we deal with (a pointer to) an int
, but what if we have a pointer to a pointer? Do you want the first pointer or the second pointer? Or dereference all the way until we get a non-pointer value? It's not even that far-fetched: what if we have a function that that operates on an iterator, and the iterator may or may not be passed via a pointer, but the iterator itself could be just a plain pointer (like std::begin()
on a C-style array)?
It would really be much better if you put the burden of dereferencing on the caller, as the caller knows best what to do.
Perhaps as a compromise, assuming we want a T
but it might be passed as T&
, T*
or std::smart_ptr<T>
, you could change dereference()
so that you have to provide the expected value type T
as a template parameter.
About supporting std::shared_ptr
This code does not support the concept of shared pointer though.
dereference
does not increment a counter, so using an obtained reference is dangerous.
If you want to do call derefence()
on a std::shared_ptr
, I would not worry about it. Your derefence()
returns a reference, so it's already required that you should keep the original object alive for the duration you are using the reference you got. This is no different from dereferencing or calling get()
on a std::shared_ptr
itself.
It does not work on raw pointers
Surprisingly, while your code works fine with smart pointers, it doesn't work with raw pointers. The problem is that the first is_pointer_like
version doesn't actually remove the pointer from T
. That's easy to fix:
template<typename T>
struct is_pointer_like : std::is_pointer<T>
{
using value_type = std::remove_pointer_t<T>;
};
It doesn't work with temporary std::unique_ptr
and std::shared_ptr
s
The following doesn't work either:
std::cout << eld::traits::dereference(std::make_unique<int>(42)) << '\n';
Even though it works if you pass in a named std::unique_ptr
variable. The reason is that while you correctly handle const
in the function dereference()
, you are missing specializations of is_pointer_type
that handle const std::unique_ptr
and const std::shared_ptr
. You have to add:
template<typename T, typename DeleterT>
struct is_pointer_like<const std::unique_ptr<T, DeleterT>> : std::true_type
{
using value_type = const T;
};
template<typename T>
struct is_pointer_like<const std::shared_ptr<T>> : std::true_type
{
using value_type = const T;
};
Adding a test suite would be helpful in finding these kinds of problems.
Simplifying the code
One issue at the moment is that you have to explicitly handle both std::shared_ptr
and std::unique_ptr
, and const
and non-const
versions of both, duplicating a lot of code. With SFINAE or C++20 concepts, it might be possible to make an is_pointer_like
that handles both smart pointers in the same way, and might even support other smart point types that have the same interface. In particular, both std::shared_ptr
and std::unique_ptr
have a member type element_type
, and a member function get()
. It would then also work with the deprecated std::auto_ptr
. Ideally it looks like:
template<typename T>
requires is_smart_pointer_like<T>
struct is_pointer_like<T> : std::true_type
{
// Deduce the value type such that const is propagated
using value_type = std::remove_reference_t<decltype(*std::declval<T&>())>;
};
Then the trick is just to implement the concept is_smart_pointer_like
.
But note that there is nothing in the body that uses element_type
or get()
, it only needs T
to be dereferencable. So perhaps you could even just check for that, as it will then also work for regular pointers, but on the other hand this might match more types than you like. For example, it would then match iterators.
*
somewhere in the code? \$\endgroup\$void do_stuff
, I need to invoke a member function of T. But I don't want to limit T to be either a reference or a pointer. Because member invokation for the latter requires dereferencing. So template for a pointer would not compile for a reference. Hence I want an adapter that will access the member and compile in both cases. \$\endgroup\$do_stuff(*foo)
when using with a pointer? (I'm assuming that's a function similar toso_stuff()
in the question) Is it becausefoo
has come from dereferencing an iterator or something, where the caller can't know whether it needs to dereference for itself? I'm asking because that might make a difference to the object-lifetime concerns you mentioned. \$\endgroup\$the caller can't know whether it needs to dereference for itself
exactly. In case you have a complex template logic and you assume that at some point you get an object which might be a pointer, or not. It will facilitate, for example, modification of containers. You would be able to change value type to be a pointer, without changing the logic (adding or removing dereferencing) \$\endgroup\$