Make basic_string_view’s range construction conditionally explicit

Published Proposal,

SG18, LEWG, SG16, SG9
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++


We propose to relax the range constructor of basic_string_view so that that particular construction is implicit when used together with string types.

1. Changelog

2. Motivation and Scope

[P1989R2] added a new constructor to basic_string_view that made it implictly constructible from any compatible range (contiguous, sized, that uses the same character type as the string view, and so on; cf. [string.view.cons]/11).

[P2499R0] subsequently modified this constructor, by making it unconditionally explicit.

The reasoning for this change was that not all compatible ranges are convertible to a string view by using their own size (the "range size"). In many important use cases, the size that is meant to be used is the "strlen size" (intended as the distance of a given marker, usually NUL, from the beginning of the range). In those cases, having an implicit conversion towards string views may end up introducing serious bugs in a program.

For instance, [P2499R0] has this example featuring "Modern C++" datatypes:

extern void get_string(std::span<char> buffer);
extern void use_string(std::string_view str);

std::array<char, 200> buf;
use_string(buf); // oops

This code used to compile before [P2499R0]. However: if the semantics of get_string is to NUL-terminate its output, use_string will instead create a view over the entire array, spanning over the embedded NUL. This opens the door to any sort of bug (incl. UB, by reading uninitialized data in the array), and could have security implications. To prevent this from happening, the conversion from buf to std::string_view has been made explicit, and the example code above is now ill-formed.

This change has a few consequences.

First and foremost it’s worth underlying that the proposed solution is still somehow a band-aid for this sort of use cases. If one uses std::string s(200); as a storage type instead of some other contiguous container (like std::array<char, 200>), then the example above would still have the problem of using the "wrong" size when s is converted to a std::string_view. The conversion from std::string to std::string_view remains however implicit.

Second, and more in general; in a sense, the proposed change splits the types that can be converted to a string view in two families:

  1. types for which we now axiomatically establish that their size is always the "range size", and these convert implictly (e.g. std::string);

  2. types for which don’t necessarily know which one is the "correct" size, and those must be converted explicitly after [P2499R0] (e.g. std::vector<char>).

The problem with split (and in particular with 2.) is that it fundamentally kills the usability of string views as an universal interface type for read-only non-owning string-like inputs.

For instance, if one has a string type different than std::string, then using it with a std::string_view becomes awkward:

void use(std::string_view v);

std::string str1;
use(str1);  // OK

my::string str2;
use(str2);  // ERROR

Unless my::string itself provides an implicit conversion operator towards std::string_view, the code above is ill-formed, requiring an explicit conversion instead.

So, what’s the problem, just provide the operator, like std::string itself does? That’s not so simple: the user may not be in control of my::string to begin with. For instance, my::string may belong to a 3rd party library (Boost, Qt, folly, and so on); every sufficiently big codebase has its own string class(es).

This usability impedance is there also if one goes in the opposite direction. Suppose one has a std::string object, and wants to use it together with a 3rd party string library. That library also defines its own string view types, and following the Standard Library’s lead, these string view types feature explicit construction from compatible ranges.


void lib::very_useful_algorithm(lib::string_view v);

std::string str;
lib::very_useful_algorithm(str);  // ERROR

There is absolutely no reason why the above shouldn’t compile. std::string and lib::string_view are perfectly compatible, with lib::string_view representing the same "platonic concept" of a non-owning const std::string. It should be therefore be implictly convertible.

Alas, that’s not the case (due to explicit construction from a range), and since we cannot modify either datatype (both belong to 3rd parties), we must add explicit conversions everywhere. This is completely anti-ergonomic for users.

The idea of adding conversion constructors/operators also does not scale. Again assuming that any 3rd party string view implementation follows [P2499R0]'s design, should the author of lib::string add implicit conversion operators towards lib::string_view, std::string_view, boost::string_view, QUtf8StringView, llvm::StringRef, ...? This is poor design (requires O(N²) conversion constructors/operators), and obviously it’s completely impossible to implement in practice (unless one controls all the classes in question).

In Qt, the author implemented the same semantic change of P2499R0 for Qt’s string view classes. The result was that certain implicit conversions got broken, such as from std::u16string_view to QStringView, despite the two classes representing exactly the same concept (a view over UTF-16/char16_t data).

What we are highlighting here is that there is tension between the safety concerns behind the changes proposed by [P2499R0]'s, and the usefulness of string views as a vocabulary interface type -- usefulness that gets dramatically reduced (or removed, one could say) by making the range conversion unconditionally explicit.

We could restore most of this usefulness by making the range conversion implicit in the cases where we know it’s safe to do. The only problematic aspect is: how to identify those cases?

3. Design Decisions

3.1. How to identify a string(view) class?

Unfortunately there isn’t an universal way to identify all and only string(view)-like classes for which we’d like to enable implicit conversions towards string views. As far as their range properties are concerned, a some_lib::string and a std::vector<char> are identical -- both are contiguous, sized, etc.; a view class like basic_string_view cannot distinguish between them based on those range properties alone.

We need therefore a trait, which necessarily needs to be opt-in. The point is that we want to err on the side of caution: for an arbitrary contiguous range we want to keep the conversion explicit. A string(view)-like class can enable the opt-in and become implicitly convertible instead, and string view classes such as basic_string_view<T> can be adapted to accept implicit conversions from compatible ranges that have the trait enabled.

There are two alternatives for the design of such a trait.

  1. We can introduce a brand new type trait to specialize, and/or a class to inherit from, similar to various other precedents in the Standard Library (for instance, std::ranges::enable_view<T>). Any string(view)-like class can then enable the trait by specializing/inheriting.

  2. We can use some "unique" feature of string(view)-like types. Specifically, this paper proposes to detect the presence and compatibility of an inner traits_type type in order to classify a compatible range as a string(view)-like type. This idea was originally proposed as an option by [P2499R0] but ultimately not chosen; at the time of this writing we do not know the exact reasons.

Both of these options have some downsides.

Given the results above, the author’s preference would be for option 2, but we would like to gather feedback from SG9, SG16 and LEWG.

3.2. Is this a defect fix?

If option 2 is chosen, we would prefer that the changes proposed by this paper would be considered as a defect fix by implementors and backported to their C++23 modes. For this reason we would not be proposing any new feature-testing macro, nor to bump the value of an existing one. (The change is still entirely detectable by users via type traits, should the need arise.)

Option 1 is fundamentally a brand new API, so it cannot be considered a fix.

4. Impact on the Standard

This proposal is a pure library change. Depending on the option chosen, it may extend/change the proposed resolution to [LWG3857].

The impact is strictly positive: code that was ill-formed (after [P2499R0]) becomes well-formed.

5. Technical Specifications

All the proposed changes are relative to [N4928].

6. Proposed wording

The wording is very similar for both options outlined above. Option 1 would add a new feature-testing macro.

6.1. Option 1: introduce a dedicated type trait

Add to the list in [version.syn]:

#define __cpp_lib_is_string_view_like YYYYMML // also in <string>, <string_view>

with the YYYYMML replaced as usual (year and month of adoption of the present proposal).

Modify [string.view.synop] as follows:

  // [string.view.template], class template basic_­string_­view
  template<class charT, class traits = char_traits<charT>>
  class basic_string_view;
  template<class T>
    constexpr bool is_string_view_like = false;

  template<class charT, class traits>
    constexpr bool is_string_view_like<basic_string_view<charT, traits>> = true;

Add a new section after [string.view.literals]:

23.4 Enablers for implicit string view construction from ranges [string.view.range.construction]
  template<class T>
    constexpr bool is_string_view_like = false;
1. Remarks: Pursuant to [namespace.std], users may specialize is_string_view_like for cv-unqualified program-defined types. Such specializations shall be usable in constant expressions ([expr.const]) and have type const bool.

Modify [string.syn] as follows:

  // [basic.string], basic_­string
  template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT>>
    class basic_string;
  template<class charT, class traits, class Allocator>
    constexpr bool is_string_view_like<basic_string<charT, traits, Allocator>> = true;

Modify [string.view.template.general] as shown:

template<class R>
  constexpr explicit(see below) basic_string_view(R&& r);

Modify [string.view.cons] as shown:

template<class R>
  constexpr explicit(see below) basic_string_view(R&& r);
  1. Let d be an lvalue of type remove_cvref_t<R>.

  2. Constraints:

(12.1) — remove_cvref_t<R> is not the same type as basic_string_view,

(12.2) — R models ranges::contiguous_range and ranges::sized_range,

(12.3) — is_same_v<ranges::range_value_t<R>, charT> is true,

(12.4) — is_convertible_v<R, const charT*> is false, and

(12.5) — d.operator ::std::basic_string_view<charT, traits>() is not a valid expression , and .

(12.6) — if the qualified-id remove_reference_t<R>::traits_type is valid and denotes a type, is_same_v<remove_reference_t<R>::traits_type, traits> is true.
  1. Effects: Initializes data_­ with ranges::data(r) and size_­ with ranges::size(r).

  2. Throws: Any exception thrown by ranges::data(r) and ranges::size(r).

15. Remarks: If the qualified-id remove_reference_t<R>::traits_type is valid, denotes a type, is_same_v<remove_reference_t<R>::traits_type, traits> is true, and is_string_view_like<remove_reference_t<R>> is true, then the expression inside explicit is equivalent to false. Otherwise, it is equivalent to true.

6.2. Option 2: use a compatible inner type_traits type

Modify [string.view.template.general] as shown:

template<class R>
  constexpr explicit(see below) basic_string_view(R&& r);

Modify [string.view.cons] as shown:

template<class R>
  constexpr explicit(see below) basic_string_view(R&& r);
  1. Let d be an lvalue of type remove_cvref_t<R>.

  2. Constraints:

(12.1) — remove_cvref_t<R> is not the same type as basic_string_view,

(12.2) — R models ranges::contiguous_range and ranges::sized_range,

(12.3) — is_same_v<ranges::range_value_t<R>, charT> is true,

(12.4) — is_convertible_v<R, const charT*> is false, and

(12.5) — d.operator ::std::basic_string_view<charT, traits>() is not a valid expression , and .

(12.6) — if the qualified-id remove_reference_t<R>::traits_type is valid and denotes a type, is_same_v<remove_reference_t<R>::traits_type, traits> is true.
  1. Effects: Initializes data_­ with ranges::data(r) and size_­ with ranges::size(r).

  2. Throws: Any exception thrown by ranges::data(r) and ranges::size(r).

15. Remarks: If the qualified-id remove_reference_t<R>::traits_type is valid, denotes a type, and is_same_v<remove_reference_t<R>::traits_type, traits> is true, then the expression inside explicit is equivalent to false. Otherwise, it is equivalent to true.

Appendix A: traits_type in Boost 1.81

boost/asio/basic_deadline_timer.hpp:148:  typedef TimeTraits traits_type;boost/asio/basic_waitable_timer.hpp:170:  typedef WaitTraits traits_type;boost/asio/experimental/basic_channel.hpp:137:  typedef typename Traits::template rebind<Signatures...>::other traits_type;boost/asio/experimental/basic_concurrent_channel.hpp:137:  typedef typename Traits::template rebind<Signatures...>::other traits_type;boost/asio/experimental/detail/channel_service.hpp:299:  typedef typename Traits::template rebind<Signatures...>::other traits_type;boost/asio/experimental/detail/channel_service.hpp:396:  typedef typename Traits::template rebind<R()>::other traits_type;boost/asio/experimental/detail/channel_service.hpp:499:    traits_type;boost/asio/experimental/detail/impl/channel_service.hpp:196:      Signatures...>::traits_type traits_type;boost/asio/experimental/detail/impl/channel_service.hpp:224:      Signatures...>::traits_type traits_type;boost/asio/experimental/detail/impl/channel_service.hpp:260:      Signatures...>::traits_type traits_type;boost/asio/experimental/detail/impl/channel_service.hpp:546:      Signatures...>::traits_type traits_type;boost/beast/core/detail/ostream.hpp:122:    using traits_type = typenameboost/beast/core/detail/ostream.hpp:51:    using traits_type = typenameboost/beast/core/detail/static_ostream.hpp:30:    using traits_type = typenameboost/compute/container/basic_string.hpp:46:    typedef Traits traits_type;boost/container/string.hpp:618:   typedef Traits                                                                      traits_type;boost/context/fixedsize_stack.hpp:44:    typedef traitsT traits_type;boost/context/pooled_fixedsize_stack.hpp:127:    typedef traitsT traits_type;boost/context/posix/protected_fixedsize_stack.hpp:46:    typedef traitsT traits_type;boost/context/posix/segmented_stack.hpp:46:    typedef traitsT traits_type;boost/context/windows/protected_fixedsize_stack.hpp:38:    typedef traitsT traits_type;boost/core/detail/string_view.hpp:350:    typedef std::char_traits<Ch> traits_type;boost/coroutine/posix/protected_stack_allocator.hpp:42:    typedef traitsT traits_type;boost/coroutine/posix/segmented_stack_allocator.hpp:43:    typedef traitsT traits_type;boost/coroutine/standard_stack_allocator.hpp:41:    typedef traitsT traits_type;boost/coroutine/windows/protected_stack_allocator.hpp:35:    typedef traitsT traits_type;boost/date_time/date.hpp:62:    typedef typename calendar::date_traits_type traits_type;boost/date_time/date_parsing.hpp:145:      typedef typename std::basic_string<char>::traits_type traits_type;boost/date_time/date_parsing.hpp:314:      typedef typename std::basic_string<charT>::traits_type traits_type;boost/date_time/time_duration.hpp:285:    typedef typename base_duration::traits_type traits_type;boost/date_time/time_duration.hpp:50:    typedef rep_type traits_type;boost/date_time/time_parsing.hpp:57:    typedef typename std::basic_string<char_type>::traits_type traits_type;boost/fiber/future/packaged_task.hpp:58:        >                                       traits_type;boost/fiber/future/promise.hpp:40:        typedef std::allocator_traits< typename object_type::allocator_type > traits_type;boost/format/alt_sstream.hpp:47:            typedef Tr     traits_type;boost/geometry/srs/projections/impl/pj_ell_set.hpp:179:        typedef srs::spar::detail::ellps_traits<param_type> traits_type;boost/geometry/srs/projections/spar.hpp:1004:    typedef proj_traits<proj_type> traits_type;boost/geometry/srs/projections/spar.hpp:1021:    typedef proj_traits<typename o_proj_type::type> traits_type;boost/graph/distributed/adjacency_list.hpp:1308:      traits_type;boost/graph/vertex_and_edge_range.hpp:24:        typedef graph_traits< Graph > traits_type;boost/histogram/ostream.hpp:46:  using traits_type = typename OStream::traits_type;boost/interprocess/streams/bufferstream.hpp:274:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/bufferstream.hpp:345:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/bufferstream.hpp:417:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/bufferstream.hpp:72:   typedef CharTraits                                    traits_type;boost/interprocess/streams/vectorstream.hpp:388:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/vectorstream.hpp:469:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/vectorstream.hpp:544:   typedef typename std::basic_ios<char_type, CharTraits>::traits_type  traits_type;boost/interprocess/streams/vectorstream.hpp:76:   typedef CharTraits                        traits_type;boost/io/ostream_joiner.hpp:48:    typedef Traits traits_type;boost/iostreams/chain.hpp:422:        typedef Tr                                     traits_type; \boost/iostreams/chain.hpp:450:    typedef typename chain_type::traits_type  traits_type;boost/iostreams/detail/buffer.hpp:87:    typedef iostreams::char_traits<Ch> traits_type;boost/iostreams/detail/double_object.hpp:101:    typedef boost::call_traits<T>                  traits_type;boost/iostreams/detail/double_object.hpp:36:    typedef Metrowerks::call_traits<T>             traits_type;boost/iostreams/detail/double_object.hpp:38:    typedef boost::call_traits<T>                  traits_type;boost/iostreams/detail/double_object.hpp:59:    typedef Metrowerks::call_traits<T>             traits_type;boost/iostreams/detail/double_object.hpp:61:    typedef boost::call_traits<T>                  traits_type;boost/iostreams/detail/double_object.hpp:99:    typedef Metrowerks::call_traits<T>             traits_type;boost/iostreams/filter/grep.hpp:50:    typedef char_traits<char_type>                     traits_type;boost/iostreams/filter/gzip.hpp:463:        typedef char_traits<char>  traits_type;boost/iostreams/filter/line.hpp:46:    typedef char_traits<char_type>                       traits_type;boost/iostreams/filter/stdio.hpp:52:        typedef BOOST_IOSTREAMS_CHAR_TRAITS(Ch)                  traits_type;boost/iostreams/filter/symmetric.hpp:76:    typedef BOOST_IOSTREAMS_CHAR_TRAITS(char_type)            traits_type;boost/iostreams/invert.hpp:53:    typedef char_traits<char_type>               traits_type;boost/iostreams/read.hpp:120:        typedef BOOST_IOSTREAMS_CHAR_TRAITS(char_type)  traits_type;boost/iostreams/read.hpp:133:        typedef iostreams::char_traits<char_type>  traits_type;boost/iostreams/read.hpp:157:        typedef iostreams::char_traits<char_type>  traits_type;boost/iostreams/read.hpp:169:        typedef iostreams::char_traits<char_type>  traits_type;boost/iostreams/read.hpp:222:        typedef iostreams::char_traits<char_type>  traits_type;boost/iostreams/skip.hpp:40:    typedef iostreams::char_traits<char_type>    traits_type;boost/iostreams/stream.hpp:31:    typedef Tr                                                 traits_type;boost/iostreams/traits.hpp:277:            > traits_type;      boost/iostreams/traits.hpp:353:    typedef Tr                              traits_type; \boost/iostreams/write.hpp:81:        typedef BOOST_IOSTREAMS_CHAR_TRAITS(char_type)  traits_type;boost/iostreams/write.hpp:98:        typedef BOOST_IOSTREAMS_CHAR_TRAITS(char_type)  traits_type;boost/lexical_cast/lexical_cast_old.hpp:98:            typedef Traits traits_type;boost/log/detail/attachable_sstream_buf.hpp:61:    typedef typename base_type::traits_type traits_type;boost/log/expressions/formatter.hpp:59:    typedef typename StreamT::traits_type traits_type;boost/log/expressions/formatters/c_decorator.hpp:134:        typedef c_decorator_traits< char_type > traits_type;boost/log/expressions/formatters/c_decorator.hpp:194:    typedef aux::c_decorator_traits< char_type > traits_type;boost/log/expressions/formatters/csv_decorator.hpp:93:        typedef csv_decorator_traits< char_type > traits_type;boost/log/expressions/formatters/xml_decorator.hpp:92:        typedef xml_decorator_traits< char_type > traits_type;boost/log/sources/record_ostream.hpp:98:    typedef typename base_type::traits_type traits_type;boost/log/support/date_time.hpp:70:    typedef typename TimeDurationT::traits_type traits_type;boost/log/utility/formatting_ostream.hpp:127:    typedef TraitsT traits_type;boost/log/utility/string_literal.hpp:58:    typedef TraitsT traits_type;boost/numeric/interval/interval.hpp:45:  typedef Policies traits_type;boost/polygon/polygon_90_set_traits.hpp:56:    typedef typename traits_by_concept<T, T2>::type traits_type;boost/polygon/polygon_90_set_traits.hpp:62:    typedef typename traits_by_concept<T, T2>::type traits_type;boost/process/detail/posix/basic_pipe.hpp:34:    typedef          Traits            traits_type;boost/process/detail/windows/basic_pipe.hpp:31:    typedef          Traits            traits_type;boost/process/pipe.hpp:105:    typedef           Traits           traits_type;boost/process/pipe.hpp:306:    typedef           Traits           traits_type;boost/process/pipe.hpp:40:    typedef          Traits            traits_type;boost/process/pipe.hpp:417:    typedef           Traits           traits_type;boost/process/pipe.hpp:526:    typedef           Traits           traits_type;boost/process/v2/cstring_ref.hpp:55:    using traits_type            = Traits;boost/process/v2/environment.hpp:117:    using traits_type      = key_char_traits<char_type>;boost/process/v2/environment.hpp:208:    using traits_type      = value_char_traits<char_type>;boost/process/v2/environment.hpp:303:  using traits_type      = std::char_traits<char_type>;boost/process/v2/environment.hpp:487:    using traits_type      = key_char_traits<char_type>;boost/process/v2/environment.hpp:707:    using traits_type      = value_char_traits<char_type>;boost/process/v2/environment.hpp:934:    using traits_type      = std::char_traits<char_type>;boost/range/detail/collection_traits_detail.hpp:288:                typedef array_traits<T> traits_type;boost/regex/concepts.hpp:301:   typedef typename regex_traits_computer<Regex>::type traits_type;boost/regex/v4/basic_regex.hpp:333:   typedef traits                                traits_type;boost/regex/v4/match_results.hpp:384:      typedef ::boost::regex_traits_wrapper<typename RegexT::traits_type> traits_type;boost/regex/v4/match_results.hpp:396:      typedef ::boost::regex_traits_wrapper<typename RegexT::traits_type> traits_type;boost/regex/v5/basic_regex.hpp:322:   typedef traits                                traits_type;boost/regex/v5/match_results.hpp:355:      typedef ::boost::regex_traits_wrapper<typename RegexT::traits_type> traits_type;boost/regex/v5/match_results.hpp:367:      typedef ::boost::regex_traits_wrapper<typename RegexT::traits_type> traits_type;boost/spirit/home/karma/stream/ostream_iterator.hpp:36:        typedef Traits traits_type;boost/static_string/static_string.hpp:833:  using traits_type = Traits;boost/test/utils/basic_cstring/basic_cstring.hpp:50:    typedef ut_detail::bcs_char_traits<CharT>           traits_type;boost/test/utils/basic_cstring/basic_cstring.hpp:622:    typedef typename basic_cstring<CharT1>::traits_type traits_type;boost/test/utils/basic_cstring/compare.hpp:105:    typedef typename boost::unit_test::basic_cstring<CharT>::traits_type traits_type;boost/test/utils/nullstream.hpp:46:    typedef typename base_type::traits_type  traits_type;boost/url/grammar/string_view_base.hpp:86:    typedef std::char_traits<char> traits_type;boost/utility/string_view.hpp:58:      typedef traits                                traits_type;boost/wave/util/flex_string.hpp:1445:    typedef T traits_type;boost/wave/util/flex_string.hpp:2381:    typedef typename flex_string<E, T, A, S>::traits_type traits_type;boost/xpressive/detail/core/linker.hpp:117:        typedef typename regex_traits_type<Locale, BidiIter>::type traits_type;boost/xpressive/detail/core/matcher/charset_matcher.hpp:31:        typedef Traits traits_type;boost/xpressive/detail/core/matcher/posix_charset_matcher.hpp:33:        typedef Traits traits_type;boost/xpressive/detail/static/compile.hpp:77:        typedef typename default_regex_traits<char_type>::type traits_type;boost/xpressive/detail/static/compile.hpp:90:        typedef typename regex_traits_type<locale_type, BidiIter>::type traits_type;boost/xpressive/detail/static/transforms/as_set.hpp:107:        typedef typename Data::traits_type traits_type;boost/xpressive/detail/static/visitor.hpp:108:        typedef Traits traits_type;boost/xpressive/detail/utility/boyer_moore.hpp:42:    typedef Traits traits_type;boost/xpressive/detail/utility/chset/chset.hpp:34:    typedef Traits traits_type;boost/xpressive/regex_compiler.hpp:59:    typedef RegexTraits traits_type;

7. Acknowledgements

Thanks to KDAB for supporting this work.

All remaining errors are ours and ours only.


Informative References

Casey Carter. basic_string_view should allow explicit conversion when only traits vary. Tentatively Ready. URL: https://wg21.link/lwg3857
Thomas Köppe. Working Draft, Standard for Programming Language C++. 18 December 2022. URL: https://wg21.link/n4928
Corentin Jabot. Range constructor for std::string_view 2: Constrain Harder. 17 March 2021. URL: https://wg21.link/p1989r2
James Touton. string_view range constructor should be explicit. 7 December 2021. URL: https://wg21.link/p2499r0
String view classes: make their range constructors explicit. URL: https://codereview.qt-project.org/c/qt/qtbase/+/448981