3
\$\begingroup\$

This is a percolation of ideas which originated this SO question:

Compound pointer traits class with method generalizing make_shared and make_unique?

The std::pointer_traits class offers the following traits, basically:

Types: pointer, element_type, difference_type, and rebind (given a pointer-like type for pointing to T, get a pointer-like type for pointing to U)

Static methods:

  • to_address - get the plain vanilla T* from some fancy pointer-to-T-like class, if possible
  • pointer_to - opposite of to_address. This is a fishy method, since it's not clear what it means with respect to resource ownership.

I think it's useful to add at least two methods - for creating a pointer to a new object / array of objects, a-la std::make_shared and std::make_unique, and one for releasing ownership + getting the raw address. Here's my implementation:

#pragma once
#ifndef EXTRA_POINTER_TRAITS_HPP_
#define EXTRA_POINTER_TRAITS_HPP_

#include <memory>

template <typename Ptr>
struct extra_pointer_traits;

template <typename T>
struct extra_pointer_traits<T*>
{
    using ptr_type = T*;
    using element_type = T;

    static ptr_type make(size_t count) { return new T[count]; }
    static ptr_type make()             { return new T;        }
};

template <typename T>
struct extra_pointer_traits<T[]>
{
    using ptr_type = T[];
    using element_type = T;

    static T* make(size_t count) { return new T[count]; }
    static T* make()             { return new T;        }
};

template <typename T, class Deleter>
struct extra_pointer_traits<std::unique_ptr<T[], Deleter>>
{
    using ptr_type = std::unique_ptr<T[], Deleter>;
    using element_type = T;

    static T* release(ptr_type& ptr) { return ptr.release(); }
    static std::unique_ptr<T[], Deleter> make(size_t count)
    {
        return std::make_unique<T[]>(count);
    }
};


template <typename T, class Deleter>
struct extra_pointer_traits<std::unique_ptr<T, Deleter>>
{
    using ptr_type = std::unique_ptr<T, Deleter>;
    using element_type = T;

    static T* release(ptr_type& ptr) { return ptr.release(); }
    static std::unique_ptr<T, Deleter> make()
    {
        return std::make_unique<T>();
    }
};


template <typename T>
struct extra_pointer_traits<std::shared_ptr<T>>
{
    using ptr_type = std::shared_ptr<T>;
    using element_type = typename std::pointer_traits<ptr_type>::element_type;

    // No release()
    static std::shared_ptr<element_type[]> make(size_t count)
    {
        return std::make_shared<element_type[]>(count);
    }
    static std::shared_ptr<element_type> make()
    {
        return std::make_shared<element_type>();
    }
};

#endif // EXTRA_POINTER_TRAITS_HPP_

I'm considering suggesting something like this as an addition to the existing traits class the standards committe, but for now I've implemented it as a separate class.

Note: To clarify - these are not all possible template specializations for pointer-like classes in the standard library, there would be a few more.

\$\endgroup\$
  • \$\begingroup\$ I tried to suggest an idea to the committee but faced quite a lot of questions I didn't think of. They're also busy with all of those concepts, contracts, modules, executors, etc. I'm not saying that pushing an idea through committee is impossible, but it is extremely hard (at least for a student of my age and experience). \$\endgroup\$ – Incomputable Jul 28 '18 at 8:55
  • \$\begingroup\$ 1. Can you link to where those questions where raised? If not, perhaps put it in a pastebin? 2. If they're busy, that's fine. I mean, it's not fine, but it's not something wrong with the suggestion. 3. It may be extremely hard to push something through committee but it's much harder if you don't try :-) \$\endgroup\$ – einpoklum Jul 28 '18 at 10:38
  • \$\begingroup\$ Do you have an email? I can forward you the whole chain, because they operate using mailing lists ... \$\endgroup\$ – Incomputable Jul 28 '18 at 10:40
5
\$\begingroup\$

First, size_t should be std::size_t everywhere.

Why don't the raw pointer/array cases have release()? Granted, it won't really change any ownership in reality, but it could conceptually.

static ptr_type make(size_t count) { return new T[count]; }
static ptr_type make()             { return new T;        }

I'm not sure this interface is a good idea as is. This only supports Ts with default constructors. What about constructor arguments:

template <typename... Args>
static ptr_type make(Args&&... args)
{
    return new T{std::forward<Args>(args)...};
}

But of course, if you do that, it conflicts with the make(std::size_t) overload. So you'd need a different name for that, like make_n() or something, maybe with an interface like:

template <typename... Args>
static ptr_type make_n(std::size_t count, Args&&... args)
{
    return new T[count]{std::forward<Args>(args)...};
}

However, what about the difference between T{...} and T(...)? Now you need overloads for those cases, too.

Now, as for the overall idea, I'm not a committee member, but I can tell you with absolute certainty that this will never be accepted.

First, what actual problem in use is this supposed to solve? What is an actual use case where this is necessary and offers a significant improvement? I can't think of any situation where this will be useful. Certainly not one that's generalizable, which is mandatory for a standard library addition.

Generally speaking, memory management should be handled by containers, and containers use allocators, and this has no allocator support whatsoever.

Even worse, it hides the allocation method from client code... which is disastrous because you need to know how something was allocated to know how to DEallocate it. As it is, your interface offers a "handy" way to allocate memory, but no way to free it, and in fact goes out of its way to make code less safe by stripping ownership from smart pointers with the release() function.

In summary, I see no general gain from this, and plenty of dangerous drawbacks.

\$\endgroup\$
  • \$\begingroup\$ One could also use make_many_tag, or some other tag type to ensure that the right overload is called. \$\endgroup\$ – Incomputable Jul 28 '18 at 5:46
  • \$\begingroup\$ @Incomputable: So you'd propose four overloads (make(with_braces_tag, Args&&...), make(with_parens_tag, Args&&...), make(array_with_braces_tag, size_t, Args&&...) and make(array_with_parens_tag, size_t, Args&&...))? If I'd have to use a tag for every possible overload, I'd prefer differently named functions unless there is a very good reason to do otherwise. \$\endgroup\$ – hoffmale Jul 28 '18 at 6:39
  • \$\begingroup\$ @hoffmale, well. I thought about dropping support for braces. But you're right, that would still be inferior to differently named functions. \$\endgroup\$ – Incomputable Jul 28 '18 at 6:49
  • \$\begingroup\$ 1. "Why don't the raw pointer/array cases have release()?" You mean, a release() which does nothing? I think that would be more confusing than helpful; but maybe you can convince me otherwise. 2. Good point about construction with arguments. make_unique() and make_shared() have that, and so should I. \$\endgroup\$ – einpoklum Jul 28 '18 at 7:49
  • \$\begingroup\$ About the general point: This traits class will simply allow the same piece of code to work for shared ptr's, unique_ptr's, my_custom_unique_ptr_like_t and so on - and do allocations (and perhaps releases, although that's trickier). \$\endgroup\$ – einpoklum Jul 28 '18 at 7:52
3
\$\begingroup\$

I'm with you that std::pointer_traits and C++ standard library smart pointers in general have some drawbacks (e.g. ownership semantics of pointer_to) that might need to be addressed.

That said, I don't think that this implementation is working towards that goal:

  • Memory management: Different forms of extra_pointer_traits<T>::make acquire pointers to memory, but most specializations have no way to release said memory (and the std::unique_ptr one only releases ownership, not the backing memory).

    This is no "trivial" problem, either: When given a T* (allocated using one of the overloads of extra_pointer_traits<T*>::make), do you know whether to call delete or delete[] on it?

    And this is just the tip of the iceberg: Once allocators enter the picture there is no general way of keeping track how to delete every pointer without somehow storing a reference to the allocator with each pointer.

  • Uniformity: There's no clear standard feature set. Some specializations provide make(), others provide make(size_t), some provide release() - but there is no easy way (other than failure of compilation) to query which operations are actually available.

    For example: You cannot simply call make(), as extra_pointer_traits<std::unique_ptr<T[], Deleter>> doesn't provide it.

  • Trait deduction: Most standard library traits have an ability to deduce traits from any type as long as said type adheres to some interface (e.g. std::iterator_traits automatically gives the correct traits if a class implements value_type, pointer, reference, difference_type and iterator_category).

    There doesn't seem to be an easy way to generally deduce make() or similar for any custom pointer-like type.

  • Object construction: Ignoring the memory release problems mentioned above, some types simply don't have a default constructor (or any public constructors, for that matter), which means extra_pointer_traits<PointerType>::make overloads now compile conditionally not only on the pointer type itself, but also on the pointees type. This seems counter-intuitive on a trait that should be as general as possible.

    Yes, some of these problems (non-default constructors) could be overcome with perfect forwarding of parameters, but others (non-public constructors, T(...) vs T{...}) are unsolvable (without modifying the pointee) or require additional design space (e.g. special functions for make_array, make_with_braces, make_with_parens, ...).

    Then you'd again need special handling for some cases, e.g. if the pointee is of type void (possibly cv-qualified). This means you get an combinatorial explosion of specializations (number of pointer-like types times types requiring soecial handling).

  • Ownership semantics: extra_pointer_trais<T>::make overloads always create a new owning pointer - even for pointer types (e.g. raw pointers) that don't have any concrete ownership semantics associated with themselves. This is just waiting for ownership conflicts (and all the surrounding problems, e.g. memory leaks, use-after-free, dangling pointers, ...) to happen.

  • Incomplete specializations: std::weak_ptr as another standard library pointer-like type is missing, as are std::shared_ptr<T[]> (which might be different from std::shared_ptr<T>) and T[N].

    Also, specializations for const and volatile qualified pointer-like types are missing. What specialization would be used for a const std::unique_ptr<T>? What about const volatile T* const volatile?

  • Allocator support: Most of the time, the standard library goes out of its way to allow users to bring their own allocators, which allows for some cool features that would otherwise be unavailable (e.g. control over memory allocations at an overarching level, relocatable heaps or tracing object collection). There is currently no support for allocators inside extra_pointer_traits.

As glimpsed from this overview, the implementation introduces more problems than it solves (and the latter would have to be defined more clearly, as it currently is far too vague and open to interpretation).

As long as these points remain unaddressed (no pun intended), I can't see any committee approving of this.

Food for thoughts:

There are several orthogonal concerns that pointers have to deal with:

  • Allocation: How is memory acquired and released?

  • Lifetime: What object is at that pointer referring to?

  • Ownership: When does the memory get acquired/released and who is responsible for doing so?

  • Representation (aka address model): How is the pointer value represented in memory? Examples: An address (raw pointer), an offset (e.g. boost::offset_ptr) or segmented (16 bit far pointers).

Some of my personal gripes with the standard library arise from the fact that these concerns are not cleanly separated, e.g.

  • allocators have to deal with types instead of just managing memory (mixing of allocation and lifetime concerns). This makes it hard to reuse allocators for different types, and tightly couples them to the implementation of the type that might be placed in the allocated memory.

  • std::shared_ptr by itself is only usable with raw pointer representation (mixing ownership, representation and lifetime). There's no way to have a shared_ptr contain a boost::offset_ptr internally, or to know whether the object being pointed to is actually initialized.

  • std::unique_ptr is slightly better off: It can be used with arbitrary pointer representations (via Deleter::pointer), but it is hard to do so. It still mixes ownership and lifetime, though: There will be problems with a std::unique_ptr<T> (with T not being void) referring to uninitialized memory upon assignment/destruction.

The extra_pointer_traits implementation goes beyond that: It mixes allocation, ownership and lifetime concerns, and does so in a fashion that none of those concerns are fully realized.

\$\endgroup\$
  • \$\begingroup\$ Specific points, followed by a more general one: 1. Incomplete specializations: Should definitely add them, I should have mentioned the existing ones are just some important examples. 2. Allocators - wouldn't your criticism apply to std::make_unique() and std::make_shared as well? \$\endgroup\$ – einpoklum Jul 28 '18 at 7:30
  • \$\begingroup\$ As for allocators - I don't think this class should involve itself with them, in the same was that make_unique() and make_shared() don't. You can write a pointer-like class templated on an allocator and use its make() and release() traits I suppose. \$\endgroup\$ – einpoklum Jul 28 '18 at 7:38
  • \$\begingroup\$ The more general point is, that without these traits you have a small jumble of freestanding functions and methods of the pointer-like types in the language and the standard library. But with them, you have a better-organized jumble, which is easy(er) to extend if you write your own pointer-like classes. \$\endgroup\$ – einpoklum Jul 28 '18 at 7:38
  • \$\begingroup\$ @einpoklum: std::allocate_shared is the allocator-supporting version of std::make_shared. Yes, there is no explicit std::allocate_unique equivalent for std::unique_ptr, but that doesn't mean std::unique_ptr cannot be used with allocators: std::unique_ptr can be initialized from an already constructed pointer, and the custom deleter can use the allocator for deallocation. \$\endgroup\$ – hoffmale Jul 28 '18 at 7:46
  • \$\begingroup\$ @einpoklum: Re jumble of functions: An unusable set of better organized functions cannot replace a jumble of usable freestanding functions. The problems with freeing memory make this "better organized" jumble unusable. And that is no easy problem to solve: if a T* was allocated with an allocator, you cannot simply delete or delete[] it. Allocators are a tool with a purpose: Unifying memory acquisition and release in a well understood interface. Any attempt at providing "better" memory management facilities needs to show how they are better than allocators, or be able to make use of them. \$\endgroup\$ – hoffmale Jul 28 '18 at 7:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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