1

Lets assume I have two factory functions, one returning std::unique_ptr and the other returning std::shared_ptr:

template<class T, class... Args>
std::shared_ptr<T> createObjectS (Args... args)
{
  // running some code
  return std::make_shared<T>(args...);
}
template<class T, class... Args>
std::unique_ptr<T> createObjectU (Args... args)
{
  // running some code
  return std::make_unique<T>(args...);
}

Is it possible to combine these two functions into one using template meta programming?

motam79
  • 3,542
  • 5
  • 34
  • 60
  • How do you decide on the call-site which one to call? – tkausl Jul 11 '18 at 01:16
  • I was trying to add a template parameter: , but it did not work. – motam79 Jul 11 '18 at 01:17
  • 3
    Why do you need both? Why not just have the `unique_ptr` version since `unique_ptr` easily and efficiently converts to a `shared_ptr`? – James Adkison Jul 11 '18 at 01:28
  • 1
    @JamesAdkison • not entirely efficiently, in the sense of "as efficiently". The make_shared can do both the bookkeeping and object in one allocation, but unique_ptr to shared_ptr requires a secondary allocation for the bookkeeping overhead. – Eljay Jul 11 '18 at 01:37
  • 1
    @Eljay Both `make_shared` and conversion from `unique_ptr` to `shared_ptr` will require creation of a control block. My comment is in the same spirit as the quoted text in [this answer](https://stackoverflow.com/a/37885232/4505712). Is your point about the total number of dynamic allocations? – James Adkison Jul 11 '18 at 02:00
  • @JamesAdkison • correct, the overhead of a heap allocation far outweighs the bookkeeping constructor (can't say about the overhead of the held object's constructor... I've seen some really gnarly ones). – Eljay Jul 11 '18 at 02:20
  • 1
    @Eljay For clarity, what do you mean by "bookkeeping"? I thought you were talking about the control block, which is dynamically allocated, but now I'm not sure. – James Adkison Jul 11 '18 at 02:26
  • @JamesAdkison • with `make_shared`, there is only one memory allocation. A combination of the bookkeeping block along with the object footprint. Depending on the objects and the cost of a memory allocation. Which, granted, isn't "huge", but may be the biggest cost for small objects; for larger or complex objects, could be a drop in the bucket. – Eljay Jul 11 '18 at 02:29
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/174759/discussion-between-james-adkison-and-eljay). – James Adkison Jul 11 '18 at 02:31

2 Answers2

4

You could use SFINAE, but then I don't really see the point to have this inside a function anymore. It's pretty redundant.

#include <memory>
#include <type_traits>

template <class T, class... Args>
typename std::enable_if<
    std::is_same<T, std::shared_ptr<typename T::element_type>>::value, T>::type
createObject(Args&&... args) {
    // running some code
    return std::make_shared<typename T::element_type>(std::forward<Args>(args)...);
}
template <class T, class... Args>
typename std::enable_if<
    std::is_same<T, std::unique_ptr<typename T::element_type>>::value, T>::type
createObject(Args&&... args) {
    // running some code
    return std::make_unique<typename T::element_type>(std::forward<Args>(args)...);
}

int main() {
    auto s = createObject<std::shared_ptr<int>>(1);
    auto u = createObject<std::unique_ptr<int>>(1);
}

A little bit more compact but essentially the same idea with a scoped enum

#include <memory>

enum class ptr_t { shared, unique };

template <ptr_t P, class T, class... Args>
typename std::enable_if<P == ptr_t::shared, std::shared_ptr<T>>::type
createObject(Args&&... args) {
    // running some code
    return std::make_shared<T>(std::forward<Args>(args)...);
}
template <ptr_t P, class T, class... Args>
typename std::enable_if<P == ptr_t::unique, std::unique_ptr<T>>::type
createObject(Args&&... args) {
    // running some code
    return std::make_unique<T>(std::forward<Args>(args)...);
}

int main() {
    auto s = createObject<ptr_t::shared, int>(1);
    auto u = createObject<ptr_t::unique, int>(1);
}

In C++17 you of course use if constexpr in both cases rather than SFINAE.

#include <memory>

enum class ptr_t { shared, unique };

template <ptr_t P, class T, class... Args>
decltype(auto) createObject(Args &&... args) {
    // running some code
    if constexpr (P == ptr_t::shared) {
        return std::make_shared<T>(std::forward<Args>(args)...);
    } else if (P == ptr_t::unique) {
        return std::make_unique<T>(std::forward<Args>(args)...);
    }
}
Henri Menke
  • 10,705
  • 1
  • 24
  • 42
  • 3
    Or just drop the `shared_ptr` one since the `unique_ptr` is convertible to an `shared_ptr`. `std::shared_ptr ptr = std::make_unique();`. – tkausl Jul 11 '18 at 01:25
  • @tkausl Thank you, I wasn't aware that this conversion exists. However, I am literally answering the question here, how to combine those functions into one. – Henri Menke Jul 11 '18 at 01:29
  • I like the enum tag version. If you put the type parameter first and the tag parameter second, the tag parameter could have a default template argument, in case the code base is going to use one much more often than the other. Though there's also a plus to forcing the coder to think about which is better. – aschepler Jul 11 '18 at 01:48
  • @HenriMenke Is there a way to get a concise version (like C++17, constexpr solution) using only C++14 features? – motam79 Jul 11 '18 at 02:15
  • @motam79 Not really, you could use explicit template specializations instead of SFINAE: https://hastebin.com/raw/lefofewoha – Henri Menke Jul 11 '18 at 02:40
  • @motam79 The partial template specialization also let you easily define a default pointer type to return: https://hastebin.com/ofodabesed.cpp – Henri Menke Jul 11 '18 at 03:13
1

With specialization, you may do:

template <typename T> struct FactoryImpl;

template <typename T> struct FactoryImpl<std::unique_ptr<T>>
{
    template <typename ... Ts>
    auto operator ()(Ts&&... args) const
    {
        return std::make_unique<T>(std::forward<Ts>(args)...);
    }
};

template <typename T> struct FactoryImpl<std::shared_ptr<T>>
{
    template <typename ... Ts>
    auto operator ()(Ts&&... args) const
    {
        return std::make_shared<T>(std::forward<Ts>(args)...);
    }
};

template<class T, class... Ts>
auto createObjectS (Ts&&... args)
{
    return FactoryImpl<T>{}(std::forward<Ts>(args)...);
}

with usage:

auto s = createObject<std::shared_ptr<MyObject>>(42);
auto u = createObject<std::unique_ptr<MyObject>>(42);
Jarod42
  • 203,559
  • 14
  • 181
  • 302