Possible breaking changes for v4

Each change on this page would improve toml++, but would also break something that users rely on today. They wait for the next major version, and none of them is decided.

Library

Static path::subpath() overload

toml::path::subpath(const_iterator, const_iterator) doesn't use the path it's called on, so it could be a static member function (#310). Calls through an object would still compile.

The exported symbol would change from a const member function to a static one, so programs linked against a shared build of toml++ would need rebuilding.

Paths and empty keys

A toml::path is meant to be relative to the node it's applied to, but TOML allows empty keys, and that makes some paths ambiguous. toml::path("") is one empty key. A default-constructed path is empty, and so is one built from a string that fails to parse. An empty path selects the node it's applied to, unless that node is a value or an empty container. str() loses the . between an empty key and an index, so a..[1] prints as a.[1], which is a different path.

Some other quirks: a . directly before an index is ignored, so a.[1] is the same path as a[1]. The string overloads of at_path() select nothing for a string that fails to parse, and the path overloads select the node itself for the empty path that string gives. Assigning such a string to a path empties it, and appending one leaves the path unchanged.

The path tests pin all of this as v3.4.0 had it, and v3 keeps it that way.

How paths work, and how they should work, needs a review from the ground up. Any change here would change what existing paths select or how they print.

Reverse iteration over tables

A toml::table iterator returns its key-value pair as a proxy object stored inside the iterator. std::reverse_iterator dereferences a temporary copy of the iterator, so the reference it returns outlives the proxy, and std::views::reverse does the same. Returning the proxy by value would fix it.

The iterators' reference type would change, so loops like for (auto& [k, v] : tbl) would stop compiling.

parse_result conversions without exceptions

With exceptions disabled, toml::parse_result converts implicitly to table&, table&& and const table&, and none of the conversions is ref-qualified. So toml::table t = result; is ambiguous, and toml::table& t = toml::parse(doc); compiles and dangles. Ref-qualifying the conversions fixes both, though direct-initialisation from an rvalue stays ambiguous.

Code that takes a table&& from an lvalue parse_result, or a table& from a temporary one, would stop compiling.

Comparing date-times as instants

toml::date_time compares field by field. 1979-05-27T07:32:00Z and 1979-05-27T00:32:00-07:00 are the same instant but compare unequal, and < orders by local date and time before the offset. Offset date-times could compare as instants instead, with some rule for comparing them to local ones.

Equality of tables and arrays holding date-times would change with it, and so would the order of sorted containers of them.

Node storage

Parsing allocates once for every node, every table entry, every non-empty array, and every key or string too long for the small-string buffer. A parsed 8 MB document holds about 455,000 allocations, the fewest the current types allow. These would need fewer:

  • tables as a sorted vector of key-value pairs, with one allocation per table where std::map makes one per entry
  • tables and arrays holding their values in place, where each value is a separately allocated node today
  • keys and strings that refer into one copy of the document
  • one source path per document, where every node's source region holds a std::shared_ptr to it today, and each copy is an atomic increment

Each changes the layout of a public type. The first two would also make inserting into a table or array invalidate references to its other elements, which stay valid today, and the third would change what toml::key and toml::value<std::string> hold.

A smaller node interface

toml::node has 35 virtual functions: the destructor, type(), twelve is_*() checks, eighteen as_*() casts and three is_homogeneous() overloads. So there are ten vtables of 38 entries, for node and its nine concrete types, and each concrete type has its own copy of every override, most of them a single return statement. In a program that parses and prints, they come to as much as 20 KB.

Only type() and is_homogeneous() need to be virtual. The checks and casts could be inline functions that test type(). That changes every node vtable and removes exported virtual functions, so it breaks the shared library's ABI.

Constructors that allocate under noexcept

The default and move constructors of toml::table and toml::array are noexcept. With the MSVC STL, std::map allocates in both of its own, and in debug builds std::vector does too, so running out of memory while constructing or moving a table calls std::terminate() where it could throw std::bad_alloc.

Dropping the noexcept would let it throw. Code that tests these constructors with std::is_nothrow_* or noexcept() would see the change, and so would containers that move elements only when moving is noexcept.

Detecting _Float16

TOML_ENABLE_FLOAT16 defaults to 0, so _Float16 support is opt-in. Detecting it from the compiler's predefined __FLT16_* macros breaks GCC 7 to 12 on ARM, which define the macros in C++ without accepting the type there (#57, #178, #186). GCC 13 and newer, and Clang, define the macros only where they accept the type, so detection could be limited to them.

Turning it on by default would break code that sets TOML_SMALL_FLOAT_TYPE to _Float16, or to std::float16_t in C++23 with GCC, because the library would then define its traits for that type twice.

Unicode bare keys

With TOML_ENABLE_UNRELEASED_FEATURES, bare keys can hold the non-ASCII characters that toml-lang/toml#891 allowed. TOML backed that change out before 1.1.0 (toml-lang/toml#979), because there was no agreement on normalisation or on which characters to allow. No released or draft version of TOML has these keys, so v4 could drop them.

Documents that use such keys would stop parsing until the keys are quoted.

CMake

Minimum CMake version

The minimum is CMake 3.14. Raising it to 3.21 would let project() set PROJECT_IS_TOP_LEVEL, so cmake/project-is-top-level.cmake could go. Copying runtime DLLs next to the Windows test executables would also stop needing a version check.

It would break anyone building with CMake 3.14 to 3.20, which includes the 3.16 that Ubuntu 20.04 ships.

Option names

The options use three naming styles. Most have a lowercase prefix, like tomlplusplus_INSTALL, tomlplusplus_BUILD_LIB and tomlplusplus_INSTALL_CMAKEDIR. TOMLPLUSPLUS_BUILD_MODULES is all uppercase, and BUILD_EXAMPLES and BUILD_FUZZER have no prefix. CMake convention is all uppercase, so every option would become TOMLPLUSPLUS_*.

Each renamed option breaks the scripts and recipes that set it, and one of them lives outside this repository. The OSS-Fuzz builder image copies fuzzing/build.sh from master, and CIFuzz runs master's copy against every pull request. BUILD_FUZZER has to keep working until master's script stops using it.

Package files for header-only installs

A header-only install puts the CMake package files in lib/cmake/tomlplusplus. They are the same on every architecture, so share/cmake/tomlplusplus suits them better, and Meson already uses it for header-only installs.

find_package() searches both locations. Anything that hard-codes the current path, such as a tomlplusplus_DIR setting, would break.

CPack

The top-level build includes CPack with no configuration, which adds a package target nobody has set up. Removing it takes that target away from anyone who runs it.

Meson

Version check

The tomlplusplusConfigVersion.cmake that Meson installs accepts any newer version, so find_package(tomlplusplus 2) finds 3.4. The one CMake installs only accepts the same major version, which matches the ABI namespace.

Making Meson's match would refuse version requests that succeed today.

Installed examples

With -Dbuild_examples=true, Meson installs the example programs into bin/. They exist to show the API, and the CMake build doesn't install them. Stopping it would remove files from installs that include them today.

Library type as a subproject

meson.build sets default_library=shared in its default_options. That is Meson's default anyway, so it only matters when toml++ is a subproject. There it overrides the parent project's default_library, and with Meson older than 1.8 it also overrides -Ddefault_library on the command line. Without it, the parent chooses.

Parent projects that build the library with build_lib=true and ask for static libraries would then get a static toml++, where they get a shared one today.

Repository

Tipi.build

The .tipi/ directory and the Tipi.build instructions in the README and on the main page support a package manager the project doesn't test against. Removing them would break Tipi users.