Skip to content

CXX-3232 add bsoncxx v1 declarations #1412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: master
Choose a base branch
from

Conversation

eramongodb
Copy link
Contributor

@eramongodb eramongodb commented May 28, 2025

Resolves CXX-3232. Followup to #1318, #1401, and #1402.

This is 2 out of an estimated 7 major PRs which in total are expected to resolve CXX-2745.

This PR introduces the very first set of v1 ABI exported symbols. 🎉

image

... only because MSVC complains otherwise. The rest will come as planned in CXX-3233 (impls).


Due to the volume of changes relative to the equivalent v_noabi interfaces, the bulk of relative changelog (v_noabi -> v1) and rationale for design decisions are documented inline as GitHub comments (coincidentally, exactly 100 comments in total).

In summary, notable changes include:

  • General Notes
    • Nearly all preconditions are now documented and well-defined to instead throw an exception or yield an "invalid" state.
    • Narrowing casts and discarded return values are now thoroughly accounted for as new error codes.
    • All potentially-thrown exceptions are now documented and include the list of error codes which may be thrown.
  • v1::error
    • All exceptions thrown by library components have a corresponding error code enumeration declared in v1::error with corresponding error category declared in v1::error::category.
    • Error conditions source and type are provided as the primary user-facing error code query mechanism when component-specific error codes are not necessary.
  • v1::types::b_*
    • All types are now consistently non-aggregate for list-initialization consistency.
    • All "single underlying value" types now consistently support implicit conversion to said value.
    • Multi-argument constructors are no longer explicit (always favored over value).
  • v1::types::view
    • This type is now semitrivial (no user-provided non-default special member functions).
    • Conversion to this type is now favored over conversion to value when applicable.
  • v1::types::value
    • All (non-default) constructors are now explicit (b_* is always favored) so either b_* or view (cheap) is always preferred to value (expensive) when applicable.
    • v1::types::id constructors are removed in favor of b_* and converting constructors.
    • "Inline PIMPL" avoids redundant allocation while preserving encapsulation.
  • v1::element::view
    • The "invalid" state is now clarified and thoroughly documented.
    • "Inline PIMPL" leaves room for an alternative bool-based approach to "missing field" diagnostics (CXX-2120).
    • Although not semitrivial, the class is still nothrow copyable.
    • Equality comparison is defined as "same field in same BSON binary data". Relational comparison is also possible but deferred for now. Reverted.
  • v1::document::view
    • Now supports an "invalid" state without underlying BSON binary data.
  • v1::array::view
    • Now (nearly) completely defined in terms of v1::document::view.
    • API is extended for better parity with v1::document::view (CXX-2118).
  • v1::document::value
    • Now default-constructible (CXX-939), equivalent to an "invalid" a default-initialized v1::document::view with no allocation.
    • deleter_type is now a std::function<T> instead of a function pointer.
    • Customization points for to_bson and from_bson are now properly constrained.
  • v1::array::value
    • Now (nearly) completely defined in terms of v1::document::value.
    • API is extended for better parity with v1::document::view (CXX-2118).

@eramongodb eramongodb self-assigned this May 28, 2025
@eramongodb eramongodb force-pushed the cxx-abi-v1-decls branch 2 times, most recently from b52a537 to 07f460e Compare May 30, 2025 17:52
Comment on lines +29 to +31
static_assert(
is_nothrow_moveable<view::const_iterator>::value,
"bsoncxx::v1::document::view::const_iterator must be nothrow moveable");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite v1::document::view being semitrivial, its iterator is only nothrow moveable due to its internal v1::element::view data member.

Comment on lines +29 to +31
static_assert(
is_nothrow_moveable<view::const_iterator>::value,
"bsoncxx::v1::array::view::const_iterator must be nothrow moveable");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite v1::array::view being semitrivial, its iterator is only nothrow moveable due to its internal v1::element::view data member.

namespace bsoncxx {
namespace v1 {

exception::~exception() = default;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the key function for the bsoncxx::v1::exception class and the first v1 ABI symbol (along with the vtable) to be exported by the bsoncxx library. 🎉

Although this is meant a declarations-only PR, this definition is required to support successful builds with MSVC, as otherwise:

error LNK2019: unresolved external symbol "public: virtual __cdecl bsoncxx::v1::exception::~exception(void)" (??1exception@v1@bsoncxx@@UEAA@XZ) referenced in function "public: virtual void * __cdecl bsoncxx::v1::exception::`vector deleting destructor'(unsigned int)" (??_Eexception@v1@bsoncxx@@UEAAPEAXI@Z)

Comment on lines +276 to +278
deleter_type const& get_deleter() const {
return _data.get_deleter();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A non-const .get_deleter() is deliberately not provided to prevent "desync" of the deleter from the associated BSON binary data. Read-only access is nevertheless essential to support inspection of the type of underlying deleter, such as for v_noabi compatibility (e.g. "Is this deleter compatible with v_noabi::document::value::deleter_type?"). This will be explored in CXX-3234 (v_noabi refactor).

Comment on lines +314 to +319
///
/// Implicitly convert to `this->view()`.
///
/* explicit(false) */ operator v1::document::view() const {
return this->view();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asymmetry: value -> view is implicit, but view -> value is explicit.

Comment on lines +317 to +322
///
/// Implicitly convert to `this->view()`.
///
/* explicit(false) */ operator v1::types::view() const {
return this->view();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asymmetry: value -> view is implicit, but view -> value is explicit.

Comment on lines 307 to 312
///
/// Return a view of the BSON binary data as a document.
///
v1::document::view view() const {
return {_data.get(), _length};
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All member functions below are defined in terms of v1::document::view, including error codes and Doxygen documentation.

Important

The new "invalid" state of a v1::document::view allows this conversion to be well-defined even when a value is default-initialized (_data.get() == nullptr).

class value {};
class value {
private:
v1::document::value _value;
Copy link
Contributor Author

@eramongodb eramongodb May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All behavior for this class is defined in terms of v1::document::value or v1::array::view, including error codes and Doxygen documentation.

@eramongodb eramongodb marked this pull request as ready for review May 30, 2025 20:31
@eramongodb eramongodb requested a review from a team as a code owner May 30, 2025 20:31
@eramongodb eramongodb requested review from rcsanchez97, kevinAlbs and vector-of-bool and removed request for rcsanchez97 May 30, 2025 20:32
@eramongodb eramongodb changed the title CXX-3232 add v1 interface declarations CXX-3232 add bsoncxx v1 declarations May 30, 2025
Comment on lines +248 to +250
/// @note This iterator almost satisfies Cpp17ForwardIterator, but `std::iterator_traits<T>::reference` is defined as
/// `value_type`, similar to `std::vector<bool>::iterator` and `std::istreambuf_iterator<T>`. Therefore, this iterator
/// only fully satisfies Cpp17InputIterator.
Copy link
Contributor Author

@eramongodb eramongodb Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: renamed Legacy -> Cpp17 for consistency with standard spec for C++20 and newer (by P0896R4; cppreference still uses "Legacy", a leftover from when the C++20 spec was still being drafted) + restoring this comment due to the associated changes of the prior comment being lost by GitHub.


The v1 ABI interfaces address and clarify several issues with iterators that were identified during work on CXX-3082:

  • Missing const qualifiers on multiple member functions, e.g. auto const iter = doc.begin(); use(*iter); is ill-formed (?!).
  • iterator_category = std::forward_iterator_tag despite not actually satisfying the requirements of LegacyForwardIterator:
    • e.g. does not satisfy it: const_iterator -> *it -> T const&.
    • e.g. does not satisfy a == b -> std::addressof(*a) == std::addressof(*b).
  • Operations on an end iterator behavior, such as deference yielding an invalid element and increment yielding another end iterator, are underdocumented not documented at all.

v1::document::view::const_iterator (and v1::array::view::const_iterator) addresses all these issues by ensuring it properly satisfies and declares itself to be a LegacyInputIterator instead (even though it may provide some features of a LegacyForwardIterator).

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bsoncxx ABI v1 interfaces look well researched, and I really like the efforts towards better defining expected exceptions / minimizing preconditions.

///
/// Zero-initialize the byte representation.
///
/// @note The result is equivalent to `"0E-6176"`, not `"0"`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather avoid a subtle quiet behavior change between v_noabi and v1. LGTM as proposed.

///
/// Initialize with `code` and `scope`.
///
b_codewscope(v1::stdx::string_view code, v1::document::view scope) : code{code}, scope{scope} {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a code-only constructor

I support leaving as-is as a minor simplification since the code-with-scope BSON type is deprecated.

///
/// Initialize with `increment` and `timestamp`.
///
b_timestamp(std::uint32_t increment, std::uint32_t timestamp) : increment{increment}, timestamp{timestamp} {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a increment-only constructor

I support leaving it out (as proposed) unless there is a known need.


public:
///
/// Initialize as an empty view.
Copy link
Collaborator

@kevinAlbs kevinAlbs Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming: is v1::document::view initialized to an empty document intended for consistency with v_noabi::document::view?

If there were no precedent, I might rather the initialize to an invalid, but I expect that is a quiet behavior change that might be missed when migrating. So I support this even if it means a slight asymmetry between default constructed v1::document::value (invalid) and default constructed v1::document::view (empty).

Copy link
Contributor Author

@eramongodb eramongodb Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: v1::document::value now default-initializes to a valid empty BSON document, which is now symmetric with std::string behavior.


It is both for compatibility with v_noabi::document::view as well as consistency with std::string_view, which is default-initialized as an "empty string" rather than an "invalid string view".

It follows that for consistency with std::string, a default-constructed v1::document::value should also be an empty document rather than invalid. However, as noted in CXX-939 and related discussions, this complicates the allocation strategy for representing an empty BSON document (uncondition allocation? use pre-allocated storage? etc.).

We could consider leaving the currently-proposed v1::document::value semantics the same, but make the following tweak to the behavior of this->view():

v1::document::view view() const {
    if (v1::document::view const v{_data.get(), _length}) {
        return v;
    }
    return {};
}

The result of this tweak would be that a default-initialized value is equivalent to a default-initialized view (both an "empty document") even when value._data.get() == nullptr. No unconditional or special-cased allocation is required. Since all non-ownership-related accessors are implemented in terms of this->view(), the null pointer would not be "observable" to the user via normal BSON document operations except via this->release(). However, this PR did not initially propose this due concern that this may be more confusing to users than the current inconsistency between view vs. value default initialization.

Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly in favor of keeping as-is. I expect the alternative could cause confusion with release:

auto fn = [](v1::document::value doc) {
   auto len = doc.length();  // 5 for both.
   auto ptr = doc.release(); // null if default. non-null if empty-constructed.
};
fn(v1::document::value{}); // default.
uint8_t data[5] = {5, 0, 0, 0};
fn(v1::document::value{data, 5}); // empty-constructed.

IIUC the linked reports in CXX-939 (1 and 2) wanted to default construct a document::value and assign to it. It may be not commonly needed to use a default constructed document::value as an empty document.

That said, I see the argument for consistency with std::string / std::string_view. I would not object to the alternative.

Copy link
Contributor Author

@eramongodb eramongodb Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC the linked reports in CXX-939 (1 and 2) wanted to default construct a document::value and assign to it. It may be not commonly needed to use a default constructed document::value as an empty document.

I agree. The motivating use cases are common C++ patterns such as:

auto docs = std::vector<bsoncxx::document::value>(n);

docs[0] = ...;

and:

// error: no matching constructor for initialization of 'bsoncxx::v_noabi::document::value'
bsoncxx::document::value doc;

if (...) {
  doc = ...;
}

Quote:

If we permitted default construction of value, we would need to do one of the following:

  • Allow it to point to nothing (nullptr), which would break the invariant that it owns a region of memory, as well as the invariant that it contains BSON that you can view.
  • Allocate 5 bytes on the heap to hold an empty BSON document. That would retain the invariants, but it would mean that simply declaring a document::value on the stack would involve a heap allocation. In most cases, that heap allocation would be wasted, as in your example where you almost always move a new BSON value in.

So, neither of these is particularly appealing.

The currently proposed behavior is consistent with the first option (allow it to point to nothing). Despite the misgivings in the quoted discussion, because bsoncxx::v1::document::view significantly improves handling of the "invalid" state, I believe the user experience will not be as poor as prior discussions were concerned might be the case. For bsoncxx::v1::document::value, the null pointer is only observable via .release(). For a resulting bsoncxx::v1::document::view, aside from the results of .data(), .size(), and .empty() (which are all well-defined), the observable behavior is otherwise consistent with the behavior of an empty BSON document.

bsoncxx::v1::document::value doc;

auto view = doc.view();

// Inherited from value.
REQUIRE(view.data() == nullptr);
CHECK(view.size() == 0u);

// Well-defined behavior and no exceptions.
CHECK_FALSE(view.empty());
CHECK_FALSE(view);
CHECK(view.begin() == view.end());
CHECK(view.find("x") == view.end());
CHECK(view == view);

By comparison, the current v_noabi interface is very bug-prone:

bsoncxx::document::value doc{nullptr, 0u, nullptr};

auto view = doc.view();

// Inherited from value.
REQUIRE(view.data() == nullptr);
CHECK(view.size() == 0u);

CHECK_FALSE(view.empty());
// CHECK_FALSE(view); // Not supported or well-specified.
CHECK_NOTHROW(view.end());

// Bug-prone behavior: termination(!) due to internal null pointer assertions.
// CHECK_NOTHROW(view.begin());
// CHECK_NOTHROW(view.find("x"));
// CHECK(view == view);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For amongoc, I did make bson_doc (and C++ bson::document) default-construct to point to a global unique memory region 05 00 00 00 00. This does complicate reallcation/destruction since you don't want to try and free/realloc that unique region, but I felt the simplification brought by "no invalid states" and "doesn't allocate if empty" are so strong that it was worth the effort to make it work. Otherwise, user code needs to worry "is this thing in an invalid state? Is the data-pointer going to be null?"

For reference: bson/doc.h::_bson_new

OTOH, I made bson::view null-able, since having a null-pointer state is very useful for APIs that take an "optional bson object". Having a nullable view is potentially less useful in C++ where you can have a optional<bson::view>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does complicate reallcation/destruction since you don't want to try and free/realloc that unique region, but I felt the simplification brought by "no invalid states" and "doesn't allocate if empty" are so strong that it was worth the effort to make it work. Otherwise, user code needs to worry "is this thing in an invalid state? Is the data-pointer going to be null?"

If that is the case, then I would prefer to (make an effort to) be consistent with the Async C Driver PoC and support non-allocating default construction as an empty BSON document.

Since this PR already proposes a default_deleter_type, I do not think it is too much of a stretch to also add a noop_deleter (as an exported function for v_noabi compatibility and address stability). It should be fine to reuse the existing preallocated storage for v1::document::view with const_cast<std::uint8_t*> since the API of v1::document::value does not expose a non-const pointer to the BSON data. Nevertheless added a warning that attempting to modify the pointed-to data of a default-constructed value is undefined behavior.

The view constructor(s) (also used by the copy constructors) have been updated to special-case the default-initialized view to avoid allocations. However, the "owning" constructors do not special-case the default-initialized view.

Comment on lines 1150 to 1151
/// When the type of `lhs` and `rhs` compare equal but are not a supported value (not defined by @ref
/// BSONCXX_V1_TYPES_XMACRO), the result is always `true`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead, all v1 interfaces ensure equality comparison has no preconditions and cannot fail.

Confirming: is avoiding an exception intended to avoid preconditions in general? Or is equality treated specially to avoid throwing?

Copy link
Contributor Author

@eramongodb eramongodb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions applied. Open questions following first round of review include:

  • Should compile-time constants be defined as enum constants (not exported in the ABI) instead of static constexpr variables (exported in the ABI due to pre-C++17 compatibility requirements)?
  • Should v1::document::value::view() return an "empty doc" view instead of an invalid view without otherwise changing its allocation behavior (_data.get() == nullptr)? Feedback: no.
  • Should v1::document::value continue to support implicit assignment from a v1::document::view (currently requires doc = value{view};)? Feedback: no.

Comment on lines 286 to 292
friend bool operator==(view const& lhs, view const& rhs) {
if (!lhs != !rhs) {
return false;
}

return !lhs || (lhs.raw() == rhs.raw() && lhs.offset() == rhs.offset());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Reverted v1::element::view to be non-equality-comparable.

The operation is moved back into const_iterator's equality comparison operator.


public:
///
/// Initialize as an empty view.
Copy link
Contributor Author

@eramongodb eramongodb Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: v1::document::value now default-initializes to a valid empty BSON document, which is now symmetric with std::string behavior.


It is both for compatibility with v_noabi::document::view as well as consistency with std::string_view, which is default-initialized as an "empty string" rather than an "invalid string view".

It follows that for consistency with std::string, a default-constructed v1::document::value should also be an empty document rather than invalid. However, as noted in CXX-939 and related discussions, this complicates the allocation strategy for representing an empty BSON document (uncondition allocation? use pre-allocated storage? etc.).

We could consider leaving the currently-proposed v1::document::value semantics the same, but make the following tweak to the behavior of this->view():

v1::document::view view() const {
    if (v1::document::view const v{_data.get(), _length}) {
        return v;
    }
    return {};
}

The result of this tweak would be that a default-initialized value is equivalent to a default-initialized view (both an "empty document") even when value._data.get() == nullptr. No unconditional or special-cased allocation is required. Since all non-ownership-related accessors are implemented in terms of this->view(), the null pointer would not be "observable" to the user via normal BSON document operations except via this->release(). However, this PR did not initially propose this due concern that this may be more confusing to users than the current inconsistency between view vs. value default initialization.

Thoughts?

Comment on lines 1150 to 1151
/// When the type of `lhs` and `rhs` compare equal but are not a supported value (not defined by @ref
/// BSONCXX_V1_TYPES_XMACRO), the result is always `true`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter. Even if they may not be noexcept, it is usually astonishing to the user when the expression a == b can "fail". It is difficult to justify an equality comparison for a regular type having preconditions (that is, possibly UB) or to throw an exception (the postconditions could not be satisfied): that is, why should either be the case? There is an argument for comparison with "unsupported values" to throw an exception, but I believe "unspecified" is a safer and more user-friendly approach.

On that note, took this opportunity to change the documentation of comparing unsupported types from "true" to "unspecified".

Comment on lines 219 to 242
///
/// Equivalent to `to_bson(v, *this);` after default-initialization.
///
/// @par Constraints:
/// - `T` is not @ref bsoncxx::v1::document::value.
/// - `to_bson(v, *this)` is a valid and unambiguous overload found by ADL.
///
template <typename T, detail::enable_if_t<has_to_bson<T>::value>* = nullptr>
explicit value(T const& v) : value{} {
to_bson(v, *this); // ADL.
}

///
/// Equivalent to `*this = value{v}`.
///
/// @par Constraints:
/// - `T` is not @ref bsoncxx::v1::document::value.
/// - `to_bson(v, *this)` is a valid and unambiguous overload found by ADL.
///
template <typename T, detail::enable_if_t<has_to_bson<T>::value>* = nullptr>
value& operator=(T const& v) {
*this = value{v};
return *this;
}
Copy link
Contributor Author

@eramongodb eramongodb Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: reverted due to GCC compatibility issues. 😔


After further investigation, it seems that default constructibility is possible to support while preserving backward compatibility with v_noabi::document::value by making the default constructor explicit:

explicit value() = default;

This ensures that existing patterns of the form v_noabi::document::value doc({}); to workaround lack of default-constructibility continue to work as expected without leading to ambiguity with SMFs: only the view constructor's parameter can be copy-initialized with {}.

The aforementioned issues concerning the lack of constraints was confusing the default constructibility problem with view -> value implicit conversion on assignment, where v_noabi::document::value doc; doc = view; is permitted not by an operator=(view), but via operator=(T const&) [T = view] despite no to_bson overload.

An operator=(view) could be added for consistency with v_noabi, but the current API proposes omitting this overload to ensure consistency of view -> value conversion being explicit.

Thoughts?

@kevinAlbs kevinAlbs self-requested a review June 26, 2025 19:21
Copy link
Contributor Author

@eramongodb eramongodb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really sorry about the rebase. My local git config wasn't set properly and I prematurely pushed all branches (including this one) before it was ready.

Two major feedback suggestions were implemented in this latest set of changes + one major revert:

  • Use the embedded document total length field in BSON bytes instead of a separate _length data member for document and array types.
  • Default-initialize v1::document::value (and array) as an empty document (or array) with a "no-op deleter".
  • Support for v1::document::value({}) via an explicit default constructor is reverted due to GCC compatibility issues. 😢

See accompanying comments for more details.

Re-requested reviews due to significant changes.


public:
///
/// Initialize as an empty view.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does complicate reallcation/destruction since you don't want to try and free/realloc that unique region, but I felt the simplification brought by "no invalid states" and "doesn't allocate if empty" are so strong that it was worth the effort to make it work. Otherwise, user code needs to worry "is this thing in an invalid state? Is the data-pointer going to be null?"

If that is the case, then I would prefer to (make an effort to) be consistent with the Async C Driver PoC and support non-allocating default construction as an empty BSON document.

Since this PR already proposes a default_deleter_type, I do not think it is too much of a stretch to also add a noop_deleter (as an exported function for v_noabi compatibility and address stability). It should be fine to reuse the existing preallocated storage for v1::document::view with const_cast<std::uint8_t*> since the API of v1::document::value does not expose a non-const pointer to the BSON data. Nevertheless added a warning that attempting to modify the pointed-to data of a default-constructed value is undefined behavior.

The view constructor(s) (also used by the copy constructors) have been updated to special-case the default-initialized view to avoid allocations. However, the "owning" constructors do not special-case the default-initialized view.

Comment on lines 219 to 242
///
/// Equivalent to `to_bson(v, *this);` after default-initialization.
///
/// @par Constraints:
/// - `T` is not @ref bsoncxx::v1::document::value.
/// - `to_bson(v, *this)` is a valid and unambiguous overload found by ADL.
///
template <typename T, detail::enable_if_t<has_to_bson<T>::value>* = nullptr>
explicit value(T const& v) : value{} {
to_bson(v, *this); // ADL.
}

///
/// Equivalent to `*this = value{v}`.
///
/// @par Constraints:
/// - `T` is not @ref bsoncxx::v1::document::value.
/// - `to_bson(v, *this)` is a valid and unambiguous overload found by ADL.
///
template <typename T, detail::enable_if_t<has_to_bson<T>::value>* = nullptr>
value& operator=(T const& v) {
*this = value{v};
return *this;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, after further testing of downstream effects of this change, I no longer think this pattern is viable due to this GCC issue (related: CWG 1518, CWG 1630, and P0398R0). It seems the desired behavior is only implemented in GCC 13.2 and newer (July 2023), with GCC 4.8 through GCC 13.1 failing to compile due to ambiguous overload errors which the explicit is meant to prevent. Clang and MSVC do not appear to have this issue. 😞

Reverted the addition of explicit to avoid supporting the value({}) pattern for v1. The v_noabi API is (once again) not expected to support default construction.

///
/// The type of a pointer to @ref noop_deleter.
///
using noop_deleter_type = void(BSONCXX_ABI_CDECL*)(std::uint8_t*);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is useful when inspecting the deleter, such as with this->get_deleter().target<noop_deleter_type>().

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry I thought I posted comments as a review before). Latest changes LGTM with a request to restore a view constructor accepting a length (for safer migration). I support removing __cdecl.

///
/// @attention This feature is experimental! It is not ready for use!
///
BSONCXX_ABI_EXPORT_CDECL(std::error_category const&) decimal128();
Copy link
Contributor Author

@eramongodb eramongodb Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. I will apply the removal of __cdecl to v_noabi as part of the v_noabi refactor PR (CXX-3234). See below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants