Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ UTF-8 decoding is performed using a state machine based on Bjoern Hoehrmann's '[
- **[@jonestristand](https://github.com/jonestristand)** - Designed and implemented the `toml::path`s feature
- **[@kcsaul](https://github.com/kcsaul)** - Fixed a bug
- **[@levicki](https://github.com/levicki)** - Helped design some new features
- **[@mikomikotaishi](https://github.com/mikomikotaishi)** - Added support for C++20 modules
- **[@mikomikotaishi](https://github.com/mikomikotaishi)** - Added support for C++20 modules and custom strings
- **[@moorereason](https://github.com/moorereason)** - Reported a whole bunch of bugs
- **[@mosra](https://github.com/mosra)** - Created the awesome [m.css] used to generate the API docs
- **[@N-Dekker](https://github.com/N-Dekker)** - Added a workaround for the legacy lambda processor of MSVC 2019/2022, added `get_line`
Expand Down
1 change: 1 addition & 0 deletions fuzzing/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ clang++ -std=c++17 -O2 -DUSE_VENDORED_LIBS=1 \
tests/conformance_burntsushi_valid.cpp \
tests/conformance_iarna_invalid.cpp \
tests/conformance_iarna_valid.cpp \
tests/custom_string.cpp \
tests/formatters.cpp \
tests/for_each.cpp \
tests/impl_toml.cpp \
Expand Down
87 changes: 85 additions & 2 deletions include/toml++/impl/forward_declarations.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,37 @@ TOML_NAMESPACE_START // abi namespace
/// \brief The 'default' formatter used by TOML objects when they are printed to a stream.
/// \detail This is an alias for #toml::toml_formatter.
using default_formatter = toml_formatter;

/// \brief Customization point for using your own string types with toml++.
///
/// \detail Specialize this for a user-defined string type to opt it in to being usable as an initializer
/// for TOML string values, and as a target type for node::value(), node::value_or() and friends: \cpp
/// namespace toml
/// {
/// template <>
/// struct string_like<foo::String> : std::true_type
/// {};
/// }
///
/// // ... thereafter:
/// tbl.insert("hostname", foo::String{ "localhost" });
/// auto hostname = tbl["hostname"].value<foo::String>();
/// \ecpp
///
/// Opting in is necessary but not sufficient; the direction(s) in which a type may be used are
/// determined by the conversions it actually supports:
/// - to be usable as an initializer, it must be convertible to `std::string_view`
/// - to be usable as a retrieval target, it must be constructible from `std::string_view`
///
/// Types satisfying only one of the two are supported in that direction only; attempting to use one
/// in the unsupported direction is a compile error.
///
/// \note TOML string values are always stored internally as std::string. This customization point governs
/// conversions at the API boundary only, not the underlying storage, so retrieving a value as a
/// user-defined string type makes a copy.
template <typename T>
struct string_like : std::false_type

@marzer marzer Jul 28, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hi, thanks for the contribution. I can see the desire for this, but to be honest I'd much prefer an inferred model, as this is inline with nlohmann::json and other common libraries in this domain, where they detect 'string-likes'.

The basic principle would be to detect types that:

  • have public const char* data() const or const char* c_str() const
  • have public size_t length() const
  • (optionally) not implicitly convertible to string_view (to avoid ambiguity).

These sorts of checks should be pretty trivial to write with some expression SFINAE (is_detected or similar).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, I can try and look at nlohmann::json's implementation and apply it here.

{};
}
TOML_NAMESPACE_END;

Expand Down Expand Up @@ -407,12 +438,42 @@ TOML_IMPL_NAMESPACE_START
inline constexpr bool is_wide_string =
is_one_of<std::decay_t<T>, const wchar_t*, wchar_t*, std::wstring_view, std::wstring>;

// user-defined string types (see toml::string_like)

template <typename T>
inline constexpr bool is_string_like = toml::string_like<remove_cvref<T>>::value;

// ... usable as an initializer for a TOML string value.
// note that a type converting via operator std::string() is *not* convertible to std::string_view (that would
// require two user-defined conversions), so both targets have to be checked independently.
template <typename T>
inline constexpr bool string_like_is_initializer =
is_string_like<T>
&& (std::is_convertible_v<const remove_cvref<T>&, std::string_view>
|| std::is_convertible_v<const remove_cvref<T>&, std::string>);

// ... usable as a target type when retrieving a TOML string value
template <typename T>
inline constexpr bool string_like_is_target = is_string_like<T>
&& (std::is_constructible_v<remove_cvref<T>, std::string_view>
|| std::is_constructible_v<remove_cvref<T>, const std::string&>);

// ... and whether constructing that target can throw (it is built from whichever of the two the type supports,
// preferring the view since it never allocates a temporary)
template <typename T>
inline constexpr bool string_like_target_is_nothrow =
std::is_constructible_v<remove_cvref<T>, std::string_view>
? std::is_nothrow_constructible_v<remove_cvref<T>, std::string_view>
: std::is_nothrow_constructible_v<remove_cvref<T>, const std::string&>;

template <typename T>
inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v<remove_cvref<T>, std::string>
#if TOML_HAS_CHAR8
&& !std::is_same_v<remove_cvref<T>, std::u8string>
#endif

// user-defined string types are constructed from the stored
// std::string, so they can throw iff that construction can
&& !(string_like_is_target<T> && !string_like_target_is_nothrow<T>)
&& !is_wide_string<T>;

template <typename, typename>
Expand Down Expand Up @@ -507,12 +568,17 @@ TOML_IMPL_NAMESPACE_START
template <typename T>
struct value_traits;

// (defined alongside the other string traits, below)
template <typename T>
struct user_string_traits;

// note: enums cannot have conversion operators, so the enum and string-like cases are mutually exclusive
template <typename T, bool = std::is_enum_v<T>>
struct value_traits_base_selector
{
static_assert(!is_cvref<T>);

using type = default_value_traits;
using type = std::conditional_t<is_string_like<T>, user_string_traits<T>, default_value_traits>;
};
template <typename T>
struct value_traits_base_selector<T, true>
Expand Down Expand Up @@ -742,6 +808,23 @@ TOML_IMPL_NAMESPACE_START
struct value_traits<char[N]> : string_traits<char[N]>
{};

// string value_traits specializations - user-defined string types (see toml::string_like)
//
// unlike the built-in string types these are never 'native' (storage is always std::string), and the two
// directions are tracked independently, since a type may support only one of them:
// - is_losslessly_convertible_to_native => usable as an initializer (T -> std::string_view)
// - can_represent_native => usable as a retrieval target (std::string_view -> T)
template <typename T>
struct user_string_traits
{
using native_type = std::string;
static constexpr bool is_native = false;
static constexpr bool is_losslessly_convertible_to_native = string_like_is_initializer<T>;
static constexpr bool can_represent_native = string_like_is_target<T>;
static constexpr bool can_partially_represent_native = can_represent_native;
static constexpr auto type = node_type::string;
};

// string value_traits specializations - char8_t-based strings
#if TOML_HAS_CHAR8
template <>
Expand Down
24 changes: 23 additions & 1 deletion include/toml++/impl/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ TOML_DISABLE_ARITHMETIC_WARNINGS;
TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \
TOML_SA_LIST_SEP "const char*" \
TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \
TOML_SA_LIST_END
TOML_SA_LIST_END \
\
TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \
TOML_SA_LIST_BEG "must be constructible from std::string_view" \
TOML_SA_LIST_END

#define TOML_SA_VALUE_FUNC_MESSAGE(type_arg) \
"The " type_arg " must be one of:" \
Expand All @@ -68,6 +72,10 @@ TOML_DISABLE_ARITHMETIC_WARNINGS;
TOML_SA_VALUE_MESSAGE_U8STRING_VIEW \
TOML_SA_LIST_SEP "const char*" \
TOML_SA_VALUE_MESSAGE_CONST_CHAR8 \
TOML_SA_LIST_END \
\
TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like" \
TOML_SA_LIST_BEG "must be constructible from std::string_view" \
TOML_SA_LIST_END

// clang-format on
Expand Down Expand Up @@ -1018,6 +1026,16 @@ TOML_NAMESPACE_START
#endif
}

// char -> user-defined string type (see toml::string_like)
else if constexpr (string_like_is_target<T>)
{
// prefer the view; it never allocates a temporary
if constexpr (std::is_constructible_v<T, std::string_view>)
return T{ std::string_view{ str } };
else
return T{ str };
}

#if TOML_HAS_CHAR8

// char -> char8_t (potentially unsafe - the feature is 'experimental'!)
Expand Down Expand Up @@ -1244,6 +1262,10 @@ TOML_NAMESPACE_START
TOML_SA_LIST_SEP "const wchar_t*"
#endif
TOML_SA_LIST_END

TOML_SA_LIST_NXT "A user-defined string type opted-in via toml::string_like"
TOML_SA_LIST_BEG "must be constructible from std::string_view"
TOML_SA_LIST_END
);

// clang-format on
Expand Down
1 change: 1 addition & 0 deletions src/modules/tomlplusplus.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export namespace toml {
using TOML_NAMESPACE::path_component;
using TOML_NAMESPACE::source_position;
using TOML_NAMESPACE::source_region;
using TOML_NAMESPACE::string_like;
using TOML_NAMESPACE::table;
using TOML_NAMESPACE::time;
using TOML_NAMESPACE::time_offset;
Expand Down
Loading
Loading