Fixing allocator usage for operator+(basic_string)

Paper number P1165R0
Reply-to Tim Song <rs2740@gmail.com>
Audience LWG

Abstract

Allocator propagation for basic_string's operator+ is haphazard, inconsistent, and a source of implementation divergence. Let's make it consistent.

Introduction

Let lhs and rhs be two basic_string lvalues with a char value_type. The following table shows the allocator used for the result for each of the 12 operator+ overloads, in the working paper and three major implementations:

Expression WP libstdc++ (trunk) libc++ (trunk) MSVC STL (19.00.23506)
lhs + rhs SOCCC on SOCCC on lhs SOCCC on lhs lhs (no SOCCC) default-constructed
lhs + std::move(rhs) rhs rhs rhs rhs
std::move(lhs) + rhs lhs lhs lhs lhs
std::move(lhs) + std::move(rhs) lhs "or equivalently" rhs lhs lhs lhs
lhs + "str" default-constructed SOCCC on lhs lhs default-constructed
lhs + 'c' default-constructed SOCCC on lhs lhs default-constructed
std::move(lhs) + "str" lhs lhs lhs lhs
std::move(lhs) + 'c' lhs lhs lhs lhs
"str" + rhs default-constructed default-constructed rhs default-constructed
'c' + rhs default-constructed default-constructed rhs default-constructed
"str" + std::move(rhs) rhs rhs rhs rhs
'c' + std::move(rhs) rhs rhs rhs rhs

(SOCCC == select_on_container_copy_construction. The struckthrough text is proposed to be removed by P1148R0.)

This is haphazard, inconsistent, and a source of implementation divergence.

Discussion

In the discussion of LWG2402, Pablo Halpern explained the patterns we use for constructors of allocator-aware containers (modifications mine):

Copy constructors obtain the allocator to use via select_on_container_copy_construction; move constructors obtain the allocator to use by move constructing from the source's allocator (which is equivalent to copy after LWG2593).

operator+ constructs a new string, but it is not a copy or move constructor any more than basic_string(const basic_string&, size_t, size_t, const Allocator&). Its operands are simply sources of characters. It should therefore consistently use a default-constructed allocator.

As a binary operator, it is not possible to add an allocator argument to operator+. Designing a string concatenation API with allocator support is beyond the scope of this paper. Users desiring to control the allocator used for the result should therefore use the member functions of basic_string directly.

Proposed wording

This wording is relative to N4762.

Replace [string.op+] in its entirety with the following:

template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, const basic_string<charT, traits, Allocator>& rhs); template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, const charT* rhs);

1 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(lhs, Allocator()); r.append(rhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, const basic_string<charT, traits, Allocator>& rhs); template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, const charT* rhs);

2 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(std::move(lhs), Allocator()); r.append(rhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, basic_string<charT, traits, Allocator>&& rhs);

3 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(std::move(lhs), Allocator()); r.append(rhs); return r;

except that both lhs and rhs are left in valid but unspecified states.

Drafting note: The intent is to allow the implementation to move from either operand. — end drafting note

template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, basic_string<charT, traits, Allocator>&& rhs); template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const charT* lhs, basic_string<charT, traits, Allocator>&& rhs);

4 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(std::move(rhs), Allocator()); r.insert(0, lhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const charT* lhs, const basic_string<charT, traits, Allocator>& rhs);

5 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r = lhs; r.append(rhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(charT lhs, const basic_string<charT, traits, Allocator>& rhs);

6 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(1, lhs); r.append(rhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(charT lhs, basic_string<charT, traits, Allocator>&& rhs);

7 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(std::move(rhs), Allocator()); r.insert(r.begin(), lhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(const basic_string<charT, traits, Allocator>& lhs, charT rhs);

8 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(lhs, Allocator()); r.push_back(rhs); return r;
template<class charT, class traits, class Allocator> basic_string<charT, traits, Allocator> operator+(basic_string<charT, traits, Allocator>&& lhs, charT rhs);

9 Effects: Equivalent to:

basic_string<charT, traits, Allocator> r(std::move(lhs), Allocator()); r.push_back(rhs); return r;