PxxxxR0
Better, constexpr to_string

New Proposal,

This version:
https://eisenwave.github.io/cpp-proposals/constexpr-to-string.html
Issue Tracking:
Inline In Spec
Author:
Audience:
SG18, LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Source:
eisenwave/cpp-proposals

Abstract

This proposal seeks to add additional overloads to std::to_string.

1. Introduction

Since the changes in [P2587R3] have been accepted into the standard, to_string is no longer locale-dependent, but defined to forward its arguments to format like:

string to_string(T x) {
    return format("{}", x);
}

Furthermore, format("{}", x) for any floating-point type and integer type (though char and bool behave specially) forwards to to_chars(first, last, x), as specified in [format.string.std]

However, not every integer type can be used with to_string, and neither can every floating-point type. This has become an artificial restriction that makes use of to_string in generic code more fragile. Therefore, I propose expanding to_string to take any integer type and any floating-point type.

Furthermore, to_string can be made constexpr for integer types because it is indirectly defined in terms of to_chars, which is constexpr for integers since [P2291R3].

2. Motivation and scope

to_string is a frequently used function. [P2587R3] has identified ~11 thousand uses in total. Compared to format, it has the potential to be a much more lightweight dependency, and also communicates the intent "convert this to a string" more clearly than format("{}", ...).

That is to say, it is not obsoleted by format, and it deserves attention and care.

2.1. The inconveniences of to_string

In its current state, to_string has some problems that make it harder to use than necessary:

  1. to_string does not explicitly support aliases such as uint32_t, and these aliases are not guaranteed to use standard integer types. Therefore, anyone using to_string(uint32_t) is inadvertently relying on implementation details.

  2. to_string does not have overloads for the extended floating-point types in <stdfloat>. This is an artificial restriction because to_chars must support them, and to_string simply forwards to to_chars for standard floating-point types.

  3. to_string is not marked constexpr despite no longer depending on locale.

This proposal seeks to remove these inconveniences.

2.2. The sharp edges of to_string

It should be noted that to_string has a few surprising, sharp edges:

While I personally dislike this status quo, it is not within the scope of this proposal to alter the existing behavior. If the user prefers a different behavior for char and bool, they can use a different form of formatting.

to_string has never been a fully-fledged customization point for stringification, only a function which converts a handful of types. For a general stringification customization point, the user must either use format, or wrap to_string in some other function. Changing the behavior of to_string would not obsolete this, so it is very difficult to justify.

It should be decided whether semantic changes to to_string(char) and to_string(bool) are worth pursuing.

3. Impact on the standard

The overload set of to_string would be altered as follows:

string to_string(int val);
string to_string(unsigned val);
string to_string(long val);
string to_string(unsigned long val);
string to_string(long long val);
string to_string(unsigned long long val);
string to_string(float val);
string to_string(double val);
string to_string(long double val);
constexpr string to_string(/* integer type >= int */);
string to_string(/* floating-point type */);

(Analogous for to_wstring)

Note: In the original overload set, the behavior described in § 2.2 The sharp edges of to_string is a consequence of to_string(true) and to_string('a') calling to_string(int).

No existing well-formed code is made invalid, and the behavior of existing calls to to_string is not altered. This proposal only adds additional overloads for extended integer types and extended floating-point types.

4. Implementation experience

libstdc++ already implements to_string(int) as an inline function which uses the constexpr function detail::__to_chars_10_impl . Similarly, overloads for other integer types and floating-point types are inline functions which rely on a to_chars-like implementation.

Making to_string constexpr requires the addition of _GLIBCXX26_CONSTEXPR, but no major changes to existing code are necessary. This demonstrates the feasibility of implementing this proposal.

libc++ is most significantly affected because to_string is not yet an inline function. This ABI change can be mitigated with [[gnu::used]].

5. Design decisions

The overload set is altered so that the lowest minimal changes to to_string are made. Notably:

The design strategy in this proposal mirrors that in [P1467R9], which expanded the set sqrt(float), sqrt(double), sqrt(long double) to sqrt(floating-point-type) in a similar way.

5.1. constexpr challenges

There is no obstacle that would make constexpr to_string unimplementable for any type, at the time of writing.

However, there is the odd issue that to_string is defined in terms of format (which is not constexpr), which is defined in terms of to_chars (which is constexpr). This requires awkward wording which "magically" bridges this gap.

This is preferable to re-defining to_string in terms of to_chars directly because presumably, format will be constexpr sooner or later. We can then simply remove the bridge wording.

6. Proposed wording

The proposed changes are relative to the working draft of the standard as of [N4917], after additionally applying the changes described in [P2587R3].

Update subclause 17.3.2 [version.syn], paragraph 2 as follows:

#define __cpp_lib_to_string  202306L20XXXXL

In subclause 23.4.2 [string.syn], update the synopsis as follows:

  string to_string(int val);
  string to_string(unsigned val);
  string to_string(long val);
  string to_string(unsigned long val);
  string to_string(long long val);
  string to_string(unsigned long long val);
  string to_string(float val);
  string to_string(double val);
  string to_string(long double val);
  constexpr to_string(integer-type-least-int val);
  to_string(floating-point-type val);

[...]

  wstring to_wstring(int val);
  wstring to_wstring(unsigned val);
  wstring to_wstring(long val);
  wstring to_wstring(unsigned long val);
  wstring to_wstring(long long val);
  wstring to_wstring(unsigned long long val);
  wstring to_wstring(float val);
  wstring to_wstring(double val);
  wstring to_wstring(long double val);
  constexpr to_wstring(integer-type-least-int val);
  to_wstring(floating-point-type val);

In subclause 23.4.2 [string.syn], add a paragraph:

For each function with a parameter of type integer-type-least-int, the implementation provides an overload for each cv-unqualified integer type ([basic.fundamental]) whose conversion rank is that of int or greater. For each function with a parameter of type floating-point-type, the implementation provides an overload for each cv-unqualified floating-point type.

Update subclause 23.4.5 [string.conversions] as follows:

  string to_string(int val);
  string to_string(unsigned val);
  string to_string(long val);
  string to_string(unsigned long val);
  string to_string(long long val);
  string to_string(unsigned long long val);
  string to_string(float val);
  string to_string(double val);
  string to_string(long double val);
  constexpr to_string(integer-type-least-int val);
  to_string(floating-point-type val);

Returns: format("{}", val).

Remarks: Despite format not being marked constexpr, the call to format does not disqualify a call to to_string from being a constant expression.

[...]

  wstring to_wstring(int val);
  wstring to_wstring(unsigned val);
  wstring to_wstring(long val);
  wstring to_wstring(unsigned long val);
  wstring to_wstring(long long val);
  wstring to_wstring(unsigned long long val);
  wstring to_wstring(float val);
  wstring to_wstring(double val);
  wstring to_wstring(long double val);
  constexpr to_wstring(integer-type-least-int val);
  to_wstring(floating-point-type val);

Returns: format(L"{}", val).

Remarks: Despite format not being marked constexpr, the call to format does not disqualify a call to to_wstring from being a constant expression.

References

Normative References

[N4917]
Thomas Köppe. Working Draft, Standard for Programming Language C++. 5 September 2022. URL: https://wg21.link/n4917

Informative References

[P1467R9]
David Olsen, Michał Dominiak, Ilya Burylov. Extended floating-point types and standard names. 22 April 2022. URL: https://wg21.link/p1467r9
[P2291R3]
Daniil Goncharov, Karaev Alexander. Add Constexpr Modifiers to Functions to_chars and from_chars for Integral Types in Header. 23 September 2021. URL: https://wg21.link/p2291r3
[P2587R3]
Victor Zverovich. to_string or not to_string. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2587r3.html#biblio-codesearch

Issues Index

It should be decided whether semantic changes to to_string(char) and to_string(bool) are worth pursuing.