3076. basic_string CTAD ambiguity

Section: 27.4.3.3 [string.cons] Status: C++20 Submitter: Stephan T. Lavavej Opened: 2018-03-03 Last modified: 2021-02-25

Priority: Not Prioritized

View all other issues in [string.cons].

View all issues with C++20 status.

Discussion:

The following code fails to compile for surprising reasons.

#include <string>
#include <string_view>

using namespace std;

int main() 
{
   string s0;
   basic_string s1(s0, 1, 1);
   // WANT: basic_string(const basic_string&, size_type, size_type, const Allocator& = Allocator())
   // CONFLICT: basic_string(size_type, charT, const Allocator&)

   basic_string s2("cat"sv, 1, 1);
   // WANT: basic_string(const T&, size_type, size_type, const Allocator& = Allocator())
   // CONFLICT: basic_string(size_type, charT, const Allocator&)

   basic_string s3("cat", 1);
   // WANT: basic_string(const charT *, size_type, const Allocator& = Allocator())
   // CONFLICT: basic_string(const charT *, const Allocator&)
}

For s1 and s2, the signature basic_string(size_type, charT, const Allocator&) participates in CTAD. size_type is non-deduced (it will be substituted later, so the compiler can't immediately realize that s0 or "cat"sv are totally non-viable arguments). charT is deduced to be int (weird, but not the problem). Finally, Allocator is deduced to be int. Then the compiler tries to substitute for size_type, but this ends up giving int to allocator_traits in a non-SFINAE context, so compilation fails.

s3 fails for a slightly different reason. basic_string(const charT *, const Allocator&) participates in CTAD, deducing charT to be char (good) and Allocator to be int. This is an exact match, which is better than the constructor that the user actually wants (where int would need to be converted to size_type, which is unsigned). So CTAD deduces basic_string<char, char_traits<char>, int>, which is the wrong type.

This problem appears to be unique to basic_string and its heavily overloaded set of constructors. I haven't figured out how to fix it by adding (non-greedy) deduction guides. The conflicting constructors are always considered during CTAD, regardless of whether deduction guides are provided that correspond to the desired or conflicting constructors. (That's because deduction guides are preferred as a late tiebreaker in overload resolution; if a constructor provides a better match it will be chosen before tiebreaking.) It appears that we need to constrain the conflicting constructors themselves; this will have no effect on actual usage (where Allocator will be an allocator) but will prevent CTAD from considering them for non-allocators. As this is unusual, I believe it deserves a Note. This has been implemented in MSVC.

[2018-3-14 Wednesday evening issues processing; move to Ready]

[2018-06 Rapperswil: Adopted]

Proposed resolution:

This wording is relative to N4727.

  1. Edit 27.4.3.3 [string.cons] as indicated:

    basic_string(const charT* s, const Allocator& a = Allocator());
    

    -14- Requires: s points to an array of at least traits::length(s) + 1 elements of charT.

    -15- Effects: Constructs an object of class basic_string and determines its initial string value from the array of charT of length traits::length(s) whose first element is designated by s.

    -16- Postconditions: data() points at the first element of an allocated copy of the array whose first element is pointed at by s, size() is equal to traits::length(s), and capacity() is a value at least as large as size().

    -?- Remarks: Shall not participate in overload resolution if Allocator is a type that does not qualify as an allocator (23.2.2 [container.requirements.general]). [Note: This affects class template argument deduction. — end note]

    basic_string(size_type n, charT c, const Allocator& a = Allocator());
    

    -17- Requires: n < npos.

    -18- Effects: Constructs an object of class basic_string and determines its initial string value by repeating the char-like object c for all n elements.

    -19- Postconditions: data() points at the first element of an allocated array of n elements, each storing the initial value c, size() is equal to n, and capacity() is a value at least as large as size().

    -?- Remarks: Shall not participate in overload resolution if Allocator is a type that does not qualify as an allocator (23.2.2 [container.requirements.general]). [Note: This affects class template argument deduction. — end note]