Skip to content

Commit 8bb5b46

Browse files
committed
asioexec::completion_token: Conversion & Dangling References
Asio operations declare a set of completion signatures but may send different, convertible arguments upon completion. This is overwhelmingly (exclusively?) observed with differing value categories: An operation declares std::error_code but actually sends const std::error_code&. As originally added (35a3e31) asioexec::completion_token works around the above by pre-converting the arguments provided to the Asio completion handler. This ensures that the correct type with the correct cv- and ref-qualification is provided when sending the completion signal and also was intended to ensure that no operation which would be performed within stdexec::set_value is liable to throw an exception (i.e. performing a conversion which is not noexcept). The implementation-detail function used to provide the above-mentioned pre-conversion is asioexec::detail::completion_token::convert which has two overloads with the following intentions: - One which may change the cv- and ref-qualification but otherwise performs no actions (and is therefore marked noexcept), and - One which may perform any conversion (and is therefore not marked noexcept) Unfortunately as originally implemented the former overload was constrained on std::is_convertible_v which permits conversions beyond changing cv- and ref-qualification. Combined with the fact that the former overload returns a reference (since it's similar to std::forward) this meant that the function could: - Throw internally (thereby calling std::terminate as it is marked noexcept), and - Return a dangling reference (since a prvalue could be synthesized to effect the required conversion) Added reproducing tests and updated the constraints on the former overload so that it is only a candidate when the involved conversions involve changing cv- and ref-qualification (and therefore involve no potentially-throwing operations and cannot produce dangling references).
1 parent cb00893 commit 8bb5b46

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

include/asioexec/completion_token.hpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <tuple>
2727
#include <type_traits>
2828
#include <utility>
29+
#include <version>
2930
#include <asioexec/as_default_on.hpp>
3031
#include <asioexec/asio_config.hpp>
3132
#include <stdexec/execution.hpp>
@@ -69,7 +70,32 @@ namespace asioexec {
6970
};
7071

7172
template <typename T, typename U>
72-
requires std::is_same_v<T&&, U&&> || std::is_convertible_v<U&&, T&&>
73+
inline constexpr bool at_least_as_const_qualified_v = std::is_const_v<T> || !std::is_const_v<U>;
74+
template <typename T, typename U>
75+
inline constexpr bool at_least_as_volatile_qualified_v = std::is_volatile_v<T>
76+
|| !std::is_volatile_v<U>;
77+
template <typename T, typename U>
78+
inline constexpr bool at_least_as_qualified_v = at_least_as_const_qualified_v<T, U>
79+
&& at_least_as_volatile_qualified_v<T, U>;
80+
81+
template <typename T, typename U>
82+
requires
83+
#ifdef __cpp_lib_reference_from_temporary
84+
(std::is_convertible_v<U &&, T &&> && !std::reference_converts_from_temporary_v<T &&, U &&>)
85+
#else
86+
(
87+
// Just using is_base_of_v is insufficient because it always reports false for built-in types
88+
(std::is_base_of_v<std::remove_cvref_t<T>, std::remove_cvref_t<U>>
89+
|| std::is_same_v<std::remove_cvref_t<T>, std::remove_cvref_t<U>>)
90+
&&
91+
// The returned type must be at least as cv-qualified as the input type (it can be more cv-qualified)
92+
at_least_as_qualified_v<std::remove_reference_t<T>, std::remove_reference_t<U>>
93+
&& (
94+
// Reference type must agree except...
95+
(std::is_lvalue_reference_v<T> == std::is_lvalue_reference_v<T>) ||
96+
// ...special rules for const& which allows rvalues to bind thereto
97+
(std::is_lvalue_reference_v<T> && std::is_const_v<std::remove_reference_t<T>>) ))
98+
#endif
7399
constexpr T&& convert(U&& u) noexcept {
74100
return static_cast<U&&>(u);
75101
}

test/asioexec/test_completion_token.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ using namespace asioexec;
4242

4343
namespace {
4444

45+
static_assert(
46+
noexcept(detail::completion_token::convert<const int&>(std::declval<int&>())));
47+
static_assert(
48+
noexcept(detail::completion_token::convert<const int>(std::declval<int>())));
49+
static_assert(
50+
noexcept(detail::completion_token::convert<std::string>(std::declval<std::string>())));
51+
static_assert(
52+
!noexcept(detail::completion_token::convert<std::string>(std::declval<const std::string&>())));
53+
static_assert(
54+
!noexcept(detail::completion_token::convert<std::string>(std::declval<const char*>())));
55+
4556
// connect_shared and start_shared ensure the operation state's lifetime ends
4657
// within the completion signal handling of the receiver thereby ensuring any
4758
// use of the operation state by the operation after it's sent a completion

0 commit comments

Comments
 (0)