Date: Sun, 17 May 2026 23:44:31 +0800
Dear std-proposals,
I would like to get feedback on a possible library proposal for
std::basic_string.
Problem
Today, std::basic_string<CharT>::operator+=(CharT) makes expressions such
as the following well-formed:
std::string s;
s += 65; // appends char(65), commonly 'A', not "65"
s += true; // appends char(1)
s += 300; // appends a converted char value
This is surprising because the syntax reads like string concatenation, but
the operation is actually single-code-unit append through implicit
scalar-to-CharT conversion.
By contrast:
std::string s;
auto x = s + 65; // ill-formed
is rejected in ordinary implementations, because the non-member operator+
templates cannot deduce a consistent CharT from both the std::string
argument and the int argument.
So we have an unfortunate asymmetry:
s += 65; // accepted: appends one char
s + 65; // rejected
I do not think the problem is that operator+=(CharT) appends one character.
That operation is useful and should remain valid:
s += 'A'; // should remain OK
s.push_back('A');
The problem is that non-character scalar values can accidentally bind to
the CharT overload.
Prior related work
This is closely related to P2037R1, “String’s gratuitous assignment”, which
discussed the analogous issue for:
std::string s;
s = 50; // assigns one character, not "50"
This proposal idea is narrower: it concerns operator+=, not assignment.
There is also precedent for making dangerous string implicit behavior
ill-formed. P2166R1 prohibited constructing basic_string and
basic_string_view from nullptr, turning a common runtime hazard into a
compile-time error.
Proposed direction
The proposed direction is not to remove or change operator+=(CharT).
Instead, preserve:
std::string s;
s += 'A'; // OK
s += static_cast<char>(65); // OK: explicit code-unit intent
s += std::to_string(65); // OK: explicit textual intent
s += std::format("{}", 65); // OK
but reject accidental scalar append:
std::string s;
s += 65; // proposed: ill-formed
s += 0; // proposed: ill-formed
s += true; // proposed: ill-formed
s += nullptr; // proposed: ill-formed
A possible library-only mechanism would be to add deleted overloads to
basic_string, for example conceptually:
template<class T>
requires (!same_as<remove_cvref_t<T>, CharT> &&
convertible_to<T, CharT> &&
/* not a string-like argument */)
basic_string& operator+=(T&&) = delete;
basic_string& operator+=(nullptr_t) = delete;
The exact constraints are the main design question. The intent is to catch
accidental scalar-to-character conversion while preserving normal
string-like append operations.
Why not change operator+=(int) to append decimal text?
That would introduce implicit formatting into operator+=, which seems
worse. If the programmer wants textual formatting, the spelling should be
explicit:
s += std::to_string(n);
s += std::format("{}", n);
If the programmer wants to append a code unit derived from an integer, that
should also be explicit:
s.push_back(static_cast<char>(n));
Compatibility
This would be source-breaking for code that intentionally writes:
int ch = std::getchar();
if (ch != EOF)
s += ch;
The migration is straightforward:
s += static_cast<char>(ch);
// or
s.push_back(static_cast<char>(ch));
However, the real question is whether the breakage rate is acceptable. I
would expect corpus data and implementation experiments to be necessary
before this could become a serious paper.
A narrower alternative would be to reject only arithmetic and enumeration
types other than CharT, rather than every type convertible to CharT.
Questions for the list
1.
Is the asymmetry between s += 65 and s + 65 considered a defect worth
addressing, or only an unfortunate but acceptable historical artifact?
2.
Would a deleted-overload approach be acceptable in principle?
3.
Should the rejection cover all non-CharT types convertible to CharT, or
only arithmetic and enumeration types?
4.
Should this be limited to operator+=, or should a future paper also
revisit operator=(CharT), as discussed in P2037R1?
5.
Should push_back(CharT) remain unconstrained, since its name already
clearly denotes single-code-unit append?
6.
Would implementers be willing to experiment with a warning or extension
mode to gather compatibility data?
My current position is that operator+=(CharT) should remain valid, but
accidental scalar append through operator+= should become ill-formed or at
least diagnosable. The goal is to make the programmer choose explicitly
between “append a code unit” and “append the textual representation of a
value”.
Feedback would be appreciated.
Best regards,
Qirong Zhang
I would like to get feedback on a possible library proposal for
std::basic_string.
Problem
Today, std::basic_string<CharT>::operator+=(CharT) makes expressions such
as the following well-formed:
std::string s;
s += 65; // appends char(65), commonly 'A', not "65"
s += true; // appends char(1)
s += 300; // appends a converted char value
This is surprising because the syntax reads like string concatenation, but
the operation is actually single-code-unit append through implicit
scalar-to-CharT conversion.
By contrast:
std::string s;
auto x = s + 65; // ill-formed
is rejected in ordinary implementations, because the non-member operator+
templates cannot deduce a consistent CharT from both the std::string
argument and the int argument.
So we have an unfortunate asymmetry:
s += 65; // accepted: appends one char
s + 65; // rejected
I do not think the problem is that operator+=(CharT) appends one character.
That operation is useful and should remain valid:
s += 'A'; // should remain OK
s.push_back('A');
The problem is that non-character scalar values can accidentally bind to
the CharT overload.
Prior related work
This is closely related to P2037R1, “String’s gratuitous assignment”, which
discussed the analogous issue for:
std::string s;
s = 50; // assigns one character, not "50"
This proposal idea is narrower: it concerns operator+=, not assignment.
There is also precedent for making dangerous string implicit behavior
ill-formed. P2166R1 prohibited constructing basic_string and
basic_string_view from nullptr, turning a common runtime hazard into a
compile-time error.
Proposed direction
The proposed direction is not to remove or change operator+=(CharT).
Instead, preserve:
std::string s;
s += 'A'; // OK
s += static_cast<char>(65); // OK: explicit code-unit intent
s += std::to_string(65); // OK: explicit textual intent
s += std::format("{}", 65); // OK
but reject accidental scalar append:
std::string s;
s += 65; // proposed: ill-formed
s += 0; // proposed: ill-formed
s += true; // proposed: ill-formed
s += nullptr; // proposed: ill-formed
A possible library-only mechanism would be to add deleted overloads to
basic_string, for example conceptually:
template<class T>
requires (!same_as<remove_cvref_t<T>, CharT> &&
convertible_to<T, CharT> &&
/* not a string-like argument */)
basic_string& operator+=(T&&) = delete;
basic_string& operator+=(nullptr_t) = delete;
The exact constraints are the main design question. The intent is to catch
accidental scalar-to-character conversion while preserving normal
string-like append operations.
Why not change operator+=(int) to append decimal text?
That would introduce implicit formatting into operator+=, which seems
worse. If the programmer wants textual formatting, the spelling should be
explicit:
s += std::to_string(n);
s += std::format("{}", n);
If the programmer wants to append a code unit derived from an integer, that
should also be explicit:
s.push_back(static_cast<char>(n));
Compatibility
This would be source-breaking for code that intentionally writes:
int ch = std::getchar();
if (ch != EOF)
s += ch;
The migration is straightforward:
s += static_cast<char>(ch);
// or
s.push_back(static_cast<char>(ch));
However, the real question is whether the breakage rate is acceptable. I
would expect corpus data and implementation experiments to be necessary
before this could become a serious paper.
A narrower alternative would be to reject only arithmetic and enumeration
types other than CharT, rather than every type convertible to CharT.
Questions for the list
1.
Is the asymmetry between s += 65 and s + 65 considered a defect worth
addressing, or only an unfortunate but acceptable historical artifact?
2.
Would a deleted-overload approach be acceptable in principle?
3.
Should the rejection cover all non-CharT types convertible to CharT, or
only arithmetic and enumeration types?
4.
Should this be limited to operator+=, or should a future paper also
revisit operator=(CharT), as discussed in P2037R1?
5.
Should push_back(CharT) remain unconstrained, since its name already
clearly denotes single-code-unit append?
6.
Would implementers be willing to experiment with a warning or extension
mode to gather compatibility data?
My current position is that operator+=(CharT) should remain valid, but
accidental scalar append through operator+= should become ill-formed or at
least diagnosable. The goal is to make the programmer choose explicitly
between “append a code unit” and “append the textual representation of a
value”.
Feedback would be appreciated.
Best regards,
Qirong Zhang
Received on 2026-05-17 15:44:39
