2758. std::string{}.assign("ABCDE", 0, 1) is ambiguous

Section: 24.3.2.6.3 [string.assign] Status: C++17 Submitter: Marshall Clow Opened: 2016-07-30 Last modified: 2017-07-30

Priority: 1

View all other issues in [string.assign].

View all issues with C++17 status.

Discussion:

Before C++17, we had the following signature to std::basic_string:

basic_string&
  assign(const basic_string& str, size_type pos, size_type n = npos);

Unlike most of the other member functions on std::basic_string, there were not corresponding versions that take a charT* or (charT *, size).

In p0254r2, we (I) added:

basic_string&
  assign(basic_string_view<charT, traits> sv, size_type pos, size_type n = npos);

which made the code above ambiguous. There are two conversions from "const charT*", one to basic_string, and the other to basic_string_view, and they're both equally good (in the view of the compiler).

This ambiguity also occurs with the calls

insert(size_type pos1, const basic_string& str,             size_type pos2, size_type n = npos);
insert(size_type pos1, basic_string_view<charT, traits> sv, size_type pos2, size_type n = npos);

but I will file a separate issue (2757) for that.

A solution is to add even more overloads to assign, to make it match all the other member functions of basic_string, which come in fours (string, pointer, pointer + size, string_view).

[2016-08-03, Chicago, Robert Douglas provides wording]

[2016-08-05, Tim Song comments]

On the assumption that the basic_string version is untouchable, I like the updated P/R, with a couple comments:

  1. If it's constraining on is_convertible to basic_string_view, then I think it should take by reference to avoid copying T, which can be arbitrarily expensive. Both const T& and T&& should work; the question is whether to accommodate non-const operator basic_string_view()s (which arguably shouldn't exist).

  2. Minor issue: compare tests is_convertible and then uses direct-initialization syntax (which is is_constructible). They should match, because it's possible to have basic_string_view sv = t; succeed yet basic_string_view sv(t); fail.

[2016-08-05, Chicago LWG]

  1. breaking ABI was a non-starter
  2. we want to convert char const* to basic_string_view if possible
  3. we want to take the original type by reference, to avoid new user string types from being copied
  4. it is unfortunate that each length of string literal will generate a new entry in the symbol table

Given feedback from discussion, we wish to go with the is_convertible_v method, but change:

  1. the function parameter type to T const& for each overload
  2. the parameter type in is_convertible_v to T const&
  3. the initialization of sv(t) to sv = t

[2016-08, Chicago]

Fri PM: Move to Tentatively Ready

[2016-08-16, Jonathan Wakely reopens]

The P/R is not correct, the new overloads get chosen in preference to the overloads taking const char* when passed a char*:

#include <string>

int main () {
  std::string str("a");
  char c = 'b';
  str.replace(0, 1, &c, 1);
  if (str[0] != 'b')
    __builtin_abort();
}

With the resolution of 2758 this is now equivalent to:

str.replace(0, 1, string_view{&c, 1}, 1);

which replaces the character with string_view{"b", 1}.substr(1, npos) i.e. an empty string.

The SFINAE constraints need to disable the new overloads for (at least) the case of non-const value_type* arguments.

I've implemented an alternative resolution, which would be specified as:

Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.

All the overloads have the same problem.

This program prints no output in C++14, and we should make sure it prints no output in C++17 too:

#include <string>

int main()
{
  std::string str("a");
  char c[1] = { 'b' };
  str.append(c, 1);
  if (str != "ab")
    puts("bad append");
  str.assign(c, 1);
  if (str != "b")
    puts("bad assign");
  str.insert(0, c, 1);
  if (str != "bb")
    puts("bad insert");
  str.replace(0, 2, c, 1);
  if (str != "b")
    puts("bad replace");
  if (str.compare(0, 1, c, 1))
    puts("bad compare");
}

Ville and I considered "is_same_v<decay_t<T>, char*> is false" but that would still select the wrong overload for an array of const char, because the new function template would be preferred to doing an array-to-pointer conversion to call the old overload.

[2016-09-09 Issues Resolution Telecon]

Ville to provide updated wording; Marshall to implement

[2016-10-05]

Ville provides revised wording.

[2016-10 Telecon]

Ville's wording has been implemented in libstdc++ and libc++. Move this to Tentatively Ready and 2757 to Tentatively Resolved

Previous resolution [SUPERSEDED]:

This wording is relative to N4606.

  1. In 24.3.2 [basic.string] modify the synopsis for basic_string as follows:

    namespace std {
      template<class charT, class traits = char_traits<charT>,
        class Allocator = allocator<charT>>
      class basic_string {
      public:
        […]
        template<class T>
        basic_string& append(basic_string_view<charT, traits> svconst T& t,
                             size_type pos, size_type n = npos);
        […]
        template<class T>
        basic_string& assign(basic_string_view<charT, traits> svconst T& t,
                             size_type pos, size_type n = npos);
        […]
        template<class T>
        basic_string& insert(size_type pos1, basic_string_view<charT, traits> svconst T& t,
                             size_type pos2, size_type n = npos);
        […]
        template<class T>
        basic_string& replace(size_type pos1, size_type n1,
                              basic_string_view<charT, traits> svconst T& t,
                              size_type pos2, size_type n2 = npos);
        […]
        template<class T>
        int compare(size_type pos1, size_type n1,
                    basic_string_view<charT, traits> svconst T& t,
                    size_type pos2, size_type n2 = npos) const;
        […]
      };
    }
    
  2. In 24.3.2.6.2 [string.append], modify basic_string_view overload as follows:

    template<class T>
    basic_string& append(basic_string_view<charT, traits> svconst T& t,
                         size_type pos, size_type n = npos);
    

    -7- Throws: out_of_range if pos > sv.size().

    -8- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to append as the smaller of n and sv.size() - pos and calls append(sv.data() + pos, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true.

    -9- Returns: *this.

  3. In 24.3.2.6.3 [string.assign], modify basic_string_view overload as follows:

    template<class T>
    basic_string& assign(basic_string_view<charT, traits> svconst T& t,
                         size_type pos, size_type n = npos);
    

    -9- Throws: out_of_range if pos > sv.size().

    -10- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to assign as the smaller of n and sv.size() - pos and calls assign(sv.data() + pos, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true.

    -11- Returns: *this.

  4. In 24.3.2.6.4 [string.insert], modify basic_string_view overload as follows:

    template<class T>
    basic_string& insert(size_type pos1, basic_string_view<charT, traits> svconst T& t,
                         size_type pos2, size_type n = npos);
    

    -6- Throws: out_of_range if pos1 > size() or pos2 > sv.size().

    -7- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to assign as the smaller of n and sv.size() - pos2 and calls insert(pos1, sv.data() + pos2, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true.

    -8- Returns: *this.

  5. In 24.3.2.6.6 [string.replace], modify basic_string_view overload as follows:

    template<class T>
    basic_string& replace(size_type pos1, size_type n1, 
                         basic_string_view<charT, traits> svconst T& t,
                         size_type pos2, size_type n2 = npos);
    

    -6- Throws: out_of_range if pos1 > size() or pos2 > sv.size().

    -7- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to be inserted as the smaller of n2 and sv.size() - pos2 and calls replace(pos1, n1, sv.data() + pos2, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true.

    -8- Returns: *this.

  6. In 24.3.2.7.9 [string.compare], modify basic_string_view overload as follows:

    template<class T>
    int compare(size_type pos1, size_type n1,
                basic_string_view<charT, traits> svconst T& t,
                size_type pos2, size_type n2 = npos) const;
    

    -4- Effects: Equivalent to:

    {
      basic_string_view<charT, traits> sv = t;
      return basic_string_view<charT, traits>(this.data(), pos1, n1).compare(sv, pos2, n2);
    }
    

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true.

Proposed resolution:

This wording is relative to N4606.

  1. In 24.3.2 [basic.string] modify the synopsis for basic_string as follows:

    namespace std {
      template<class charT, class traits = char_traits<charT>,
        class Allocator = allocator<charT>>
      class basic_string {
      public:
        […]
        template<class T>
        basic_string& append(basic_string_view<charT, traits> svconst T& t,
                             size_type pos, size_type n = npos);
        […]
        template<class T>
        basic_string& assign(basic_string_view<charT, traits> svconst T& t,
                             size_type pos, size_type n = npos);
        […]
        template<class T>
        basic_string& insert(size_type pos1, basic_string_view<charT, traits> svconst T& t,
                             size_type pos2, size_type n = npos);
        […]
        template<class T>
        basic_string& replace(size_type pos1, size_type n1,
                              basic_string_view<charT, traits> svconst T& t,
                              size_type pos2, size_type n2 = npos);
        […]
        template<class T>
        int compare(size_type pos1, size_type n1,
                    basic_string_view<charT, traits> svconst T& t,
                    size_type pos2, size_type n2 = npos) const;
        […]
      };
    }
    
  2. In 24.3.2.6.2 [string.append], modify basic_string_view overload as follows:

    template<class T>
    basic_string& append(basic_string_view<charT, traits> svconst T& t,
                         size_type pos, size_type n = npos);
    

    -7- Throws: out_of_range if pos > sv.size().

    -8- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to append as the smaller of n and sv.size() - pos and calls append(sv.data() + pos, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.

    -9- Returns: *this.

  3. In 24.3.2.6.3 [string.assign], modify basic_string_view overload as follows:

    template<class T>
    basic_string& assign(basic_string_view<charT, traits> svconst T& t,
                         size_type pos, size_type n = npos);
    

    -9- Throws: out_of_range if pos > sv.size().

    -10- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to assign as the smaller of n and sv.size() - pos and calls assign(sv.data() + pos, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.

    -11- Returns: *this.

  4. In 24.3.2.6.4 [string.insert], modify basic_string_view overload as follows:

    template<class T>
    basic_string& insert(size_type pos1, basic_string_view<charT, traits> svconst T& t,
                         size_type pos2, size_type n = npos);
    

    -6- Throws: out_of_range if pos1 > size() or pos2 > sv.size().

    -7- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to assign as the smaller of n and sv.size() - pos2 and calls insert(pos1, sv.data() + pos2, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.

    -8- Returns: *this.

  5. In 24.3.2.6.6 [string.replace], modify basic_string_view overload as follows:

    template<class T>
    basic_string& replace(size_type pos1, size_type n1, 
                         basic_string_view<charT, traits> svconst T& t,
                         size_type pos2, size_type n2 = npos);
    

    -6- Throws: out_of_range if pos1 > size() or pos2 > sv.size().

    -7- Effects: Creates a variable, sv, as if by basic_string_view<charT, traits> sv = t. Determines the effective length rlen of the string to be inserted as the smaller of n2 and sv.size() - pos2 and calls replace(pos1, n1, sv.data() + pos2, rlen).

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.

    -8- Returns: *this.

  6. In 24.3.2.7.9 [string.compare], modify basic_string_view overload as follows:

    [Drafting note: The wording changes below are for the same part of the standard as 2771. However, they do not conflict. This one changes the definition of the routine, while the other changes the "Effects". — end drafting note]

    template<class T>
    int compare(size_type pos1, size_type n1,
                basic_string_view<charT, traits> svconst T& t,
                size_type pos2, size_type n2 = npos) const;
    

    -4- Effects: Equivalent to:

    {
      basic_string_view<charT, traits> sv = t;
      return basic_string_view<charT, traits>(this.data(), pos1, n1).compare(sv, pos2, n2);
    }
    

    -?- Remarks: This function shall not participate in overload resolution unless is_convertible_v<const T&, basic_string_view<charT, traits>> is true and is_convertible_v<const T&, const charT*> is false.