C++ Logo

std-proposals

Advanced search

Extending smart pointers and associative containers for better interoperability

From: jguegant_at <jguegant_at_[hidden]>
Date: Wed, 03 Jul 2019 21:59:44 +0200
Hi everyone,

While implementing a custom Associative Container that follows the interface of std::unordered_map, I realised that one
useful case was not covered by this interface: conditionally (if not already present) inserting a resource allocated
through a smart pointer into an associative container.

Let's pretend that you want to insert a type of service into a map only and if only this service is not already there.
Right now, I can only think of three strategies to adopt:
1)
     std::map<std::string, std::unique_ptr<service>> m;
     auto [it, result] = m.try_emplace("file_locator", std::make_unique<remote_file_locator>("8.8.8.8", "/a_folder/"));

     if (!result) {
           // print something
     }
2)
     auto it = m.find("file_locator");
     if (it == m.end()) {
         m["file_locator"] = std::make_unique<remote_file_locator>("8.8.8.8", "/a_folder/");
     } else {
          // print something
     }
3)
     // Try to emplace an empty `unique_ptr` first.
     auto [it, result] = m.try_emplace("file_locator", nullptr);

     if (result) {
         // The insertion happened, now we can safely create our remote_file_locator without wasting any performance.
         it->second = std::make_unique<remote_file_locator>("8.8.8.8", "/a_folder/");
     }

Solution 1) requires you to allocate unconditionally before trying to insert, potentially doing more work that needed.
Solution 2) requires you to do two lookups in the associative container, that is not ideal!
Solution 3) is unintuitive (two-phase init) and can put your map in a undesired state if the creation of
remote_file_locator throws an exception. Handling the exception is possible, but not trivial.

It is likely that there is an elegant solution to this problem that I missed. If so, I apologise for being misleading.
If not, I have thought it through and there are two directions I would like to explore to fix this ugliness.
Probably one of it would get more tractions for a proposal and I would appreciate your opinions before going any
further.

I had a look at how other similar languages solved that issue at the map level to avoid reinventing the wheel.
- D has the concept of lazy arguments, which permits to have a require property very similar to try_emplace but with the
second argument (the value) being created only if needed: https://dlang.org/spec/hash-map.html#properties
- Rust's entry member function will return an proxy object in the map that can be vacant or occupied:
https://doc.rust-lang.org/std/collections/hash_map/enum.Entry.html . Somehow this proxy can be conditionally assigned
with or_assign_with that takes a callable as a parameter. Which permits you to do something roughly like that:
   m.entry("file_locator").or_assign_with([]{ return std::make_unique<remote_file_locator>("8.8.8.8", "/a_folder/"); }).

I.A] My first solution was to bring lazy evaluation to C++. I have seen couple of proposals for a lazy evaluation at the
language level, but it may take some time before anything concret reaches the current standard. So I thought that a
library construct may have more chances to land in. This would be the very rough idea of it:
     template<class Factory>
     struct lazy_arg
     {
         using result_type = std::invoke_result_t<const Factory&>;

         constexpr lazy_arg(Factory&& factory)
             : factory_(std::move(factory)) {}

         constexpr operator result_type() const noexcept(std::is_nothrow_invocable_v<const Factory&>)
         {
             return factory_();
         }

         Factory factory_;
     };
     auto [it, result] = m.try_emplace("file_locator", lazy_arg([]{ return
   std::make_unique<remote_file_locator>("8.8.8.8", "/a_folder/"); }));

This solution is a bit arcane and relies on the conversion operator executing at the right place. I feel that this could
be too limitating and not user-friendly.

I.B] A more explicit version of this solution would be to add a try_emplace_with member function that takes a factory
callable as the second parameter. The factory is invoked if the key is not already present in the associative container.
This would give us the following syntax:
     auto [it, result] = m.try_emplace_with("file_locator", []{ return std::make_unique<remote_file_locator>("8.8.8.8",
   "/a_folder/"); });

II] A second solution that I find more appealing would be to extend the current smart pointer types (std::unique_ptr and
std::shared_ptr) with a new constructor. Similar to the value wrappers (std::variant, std::pair, std::optional...), this
constructor would take a new tag std::allocate_in_place<T> to indicate that the allocation of the pointee object of
concret type T is done inside the constructor of the smart pointer. This constructor would work exactly in the same way
as make_shared and make_unique when receiving arguments to construct an object. In other words:
     auto x = std::make_shared<something>("michael", "jackson");
     // Is equivalent to:
     auto x = std::shared_ptr(std::allocate_in_place<something>, "michael", "jackson");

Thanks to CTAD and the template parameter, we can let the compiler deduce the type of the object and the smart pointer
itself. But we can also decide to have a different types, in case we want to have base class for the smart pointer type
that holds a children class:
     auto x = std::shared_ptr<base>(std::allocate_in_place<children>, "michael", "jackson");

This solves the original issue rather elegantly:
     auto [it, result] = m.try_emplace("file_locator", std::allocate_in_place<remote_file_locator>, "8.8.8.8",
   "/a_folder/");

Given a bit more work on the CTAD rules, a few more overloads and maybe some extra tags, we could probably also handle
the same overloads as allocate_shared (allocator aware) and the array types. That would finally make all the make_xxx
factory functions obsolete!

I would value any feedback, opinion or comments on these two ideas!

Sincerely,
Jean Guegant

Received on 2019-07-03 15:01:40