Search code examples
c++shared-ptrsmart-pointersc++14perfect-forwarding

I want to perfect forward variadic arguments except for a specific type


I have the following

#include <iostream>
#include <memory>

template<typename _type>
class handle
{
  using ptr = std::shared_ptr<_type>;
  using pptr = std::shared_ptr<ptr>;
public:
  handle(handle<_type> const & other) :
    mData(make_pptr(*(other.mData)))
  {}


  handle(_type && data) :
    mData(make_pptr(std::move(data)))
  {}
private:
  pptr mData;

  template<typename ..._args>
  constexpr auto make_ptr(_args && ...args)
  {
    return std::make_shared<_type>(std::forward<_args>(args)...);
  }

  constexpr auto make_pptr(ptr const & pointer)
  {
    return std::make_shared<ptr>(pointer);
  }

  template<typename ..._args>
  constexpr auto make_pptr(_args && ...args)
  {
    return std::make_shared<ptr>(make_ptr(std::forward<_args>(args)...));
  }
};

int main()
{
  handle<int> h = 5;
  handle<int> h2(h);
}

Compiled with g++-4.9 --std=c++14 -O0 -o main main.cpp the code

handle<int> h2(h);

does not compile. The problem functions are all the overloads of

make_pptr

As I understand it, the template function will always be chosen, as the compiler tries to find the most specialized function call and the perfect forwarding creates exactly that.

I found the following two pages who seem to handle that problem with the type trait std::enable_if and std::is_same.

https://akrzemi1.wordpress.com/2013/10/10/too-perfect-forwarding/

http://www.codesynthesis.com/~boris/blog/2012/05/30/perfect-forwarding-and-overload-resolution/

The actual question is, how can I change this function, so that the non-template functions will be called if I pass the factory function an already existing pointer?

Is there a common way to do it?


Solution

  • As Jarod's answer explains, in the constructor

    handle(handle<_type> const & other) :
      mData(make_pptr(*(other.mData)))
    {}
    

    you call make_pptr with an argument of type shared_ptr<_type>&, which makes the perfect forwarding overload of make_pptr a better match than the one that takes a shared_ptr<_type> const&. You can cast the argument to const& as he shows, or you could add another overload of make_pptr that takes a non-const lvalue reference.

    constexpr auto make_pptr(ptr & pointer)
    {
      return std::make_shared<ptr>(pointer);
    }
    

    Yet another option is to constrain the perfect forwarding overload so that it is viable only when the first argument of the parameter pack is not a shared_ptr<_type>.

    Some helpers to evaluate whether the first type in the parameter pack is a shared_ptr<T>

    namespace detail
    {
        template<typename... _args>
        using zeroth_type = typename std::tuple_element<0, std::tuple<_args...>>::type;
    
        template<typename T, bool eval_args, typename... _args>
        struct is_shared_ptr
        : std::false_type
        {};
    
        template<typename T, typename... _args>
        struct is_shared_ptr<T, true, _args...>
        : std::is_same<std::decay_t<zeroth_type<_args...>>,
                       std::shared_ptr<T>
                      >
        {};
    }
    

    Then constrain the perfect forwarding make_pptr as follows

    template<typename ..._args,
             typename = std::enable_if_t<
                            not detail::is_shared_ptr<_type, sizeof...(_args), _args...>::value
                        >
            >
    constexpr auto make_pptr(_args && ...args)
    {
      return std::make_shared<ptr>(make_ptr(std::forward<_args>(args)...));
    }
    

    I also had to change your make_ptr overload because the way you have it defined in your example requires that _type be constructible from nullptr.

    constexpr auto make_ptr()
    {
      return std::make_shared<_type>();
      // no nullptr arg above, shared_ptr default ctor will initialize _type* to nullptr
    }
    

    Live demo