chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.88k stars 1.21k forks source link

use_future is not compatible with "uses-allocator construction" allocators #1358

Open amerry opened 1 year ago

amerry commented 1 year ago

This code does not compile (in C++20 mode; the same effect can be seen with std::pmr::polymorphic_allocator<void> in C++17):

#include <future>
#include <memory_resource>
#include <boost/asio/use_future.hpp>

int main() {
    std::pmr::synchronized_pool_resource memory_pool;
    boost::asio::use_future_t<std::pmr::polymorphic_allocator<>> const use_future{&memory_pool};
    using Completion = boost::asio::async_completion<decltype(use_future), void(int)>;
    Completion init(use_future);
    return 0;
}

The key part of the error output is this:

/usr/include/c++/11/bits/uses_allocator_args.h:78:29: error: static assertion failed: construction with an allocator must be possible if uses_allocator is true
  78 |               static_assert(is_constructible_v<_Tp, _Args..., const _Alloc&>,
     |                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/11/bits/uses_allocator_args.h:78:29: note: ‘std::is_constructible_v<std::promise<int>, const std::allocator_arg_t&, std::pmr::polymorphic_allocator<char>&, const std::pmr::polymorphic_allocator<char>&>’ evaluates to false

Note that the allocator is appearing twice in the arguments to the std::promise<int> constructor!

The issue is this line in impl/use_future.hpp:

    p_ = std::allocate_shared<std::promise<T>>(b, std::allocator_arg, b);

Indeed, this also fails to compile in the same way:

#include <future>
#include <memory_resource>

int main() {
    std::pmr::synchronized_pool_resource memory_pool;
    auto const promise = std::allocate_shared<std::promise<int>>(std::pmr::polymorphic_allocator<>{&memory_pool}, std::allocator_arg, std::pmr::polymorphic_allocator<>{&memory_pool});
    return 0;
}

This appears to be because std::allocate_shared uses the allocator's construct method to create the object held by the shared pointer, and std::pmr::polymorphic_allocator does uses-allocator construction, which means it forwards the top-level (shared pointer) allocator to the constructor of any type T that defines std::uses_allocator<T>::value to be true (which it is for std::promise).

The correct way to call std::allocate_shared with these types is:

#include <future>
#include <memory_resource>

int main() {
    std::pmr::synchronized_pool_resource memory_pool;
    auto const promise = std::allocate_shared<std::promise<int>>(std::pmr::polymorphic_allocator<>{&memory_pool});
    return 0;
}

and let the machinery of the standard library figure out how to pass the allocator on to std::promise<int>.

I suspect the fix is a bit more complex than that, however - I'm not sure that there's a good way to detect whether the allocator does uses-allocator construction or not, and simply dropping these arguments may cause existing code using custom allocators to silently start allocating the promise's shared state using global new.