1
\$\begingroup\$

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.

\$\endgroup\$
5
  • \$\begingroup\$ Can you explain the motivation for this a bit more? I'm guessing that it's a helper for other templates which you want to use with pointer-like types, but at some point there has to be some invocation, so is this just a matter of saving someone a * somewhere in the code? \$\endgroup\$ Sep 25, 2021 at 8:28
  • \$\begingroup\$ @TobySpeight you are correct, it is a utility to be used within a tempate function. As you can see from an example 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\$ Sep 25, 2021 at 8:49
  • \$\begingroup\$ Why not call do_stuff(*foo) when using with a pointer? (I'm assuming that's a function similar to so_stuff() in the question) Is it because foo 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\$ Sep 25, 2021 at 8:55
  • \$\begingroup\$ @TobySpeight 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\$ Sep 25, 2021 at 8:58
  • \$\begingroup\$ I think that what you want is somewhere buried in here: github.com/CarloWood/ai-utils/blob/master/print_using.h .. but no time to extract it for you. \$\endgroup\$
    – Carlo Wood
    Sep 26, 2021 at 19:04

1 Answer 1

2
\$\begingroup\$

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_ptrs

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.