Why can't the std::string constructor take just a char?

Viewed 139

While using a function template, I was looking at how to convert a char into a length 1 string in C++, and I saw that std::string(1, c) converted the char into a string using the fill constructor, following the logic of "repeat char c 1 time to form a string." However, there is no constructor overload defined for std::string that takes in just a char and turns it into a std::string.

Why is this, and what was the design decision behind not including this constructor? It feels like a very natural and intuitive idea to be able to convert a char into a string directly rather than having to use a fill constructor.

My goal is to be able to use chars and std::strings uniformly.

2 Answers

A constructor that accepts a char would accept any integral type via an implicit conversion, so that std::string(48) would work but (on typical modern systems) give you "0". You could constrain the constructor:

namespace std {
  template<
    class CharT,
    class Traits = char_traits<CharT>,
    class Allocator = allocator<CharT>
  > class basic_string {
  public:
    template<class X, class = enable_if_t<is_same_v<X, CharT>>
    basic_string(X);
    // ...
  };
}

but that doesn't seem like a simple interface anymore.

In C and C++, char serves double-duty. It represents both a character and a number. It is considered an integral type, which means that it participates in implicit integer promotion to many other integral types, as well as implicit conversion from other integral types.

Because char is overloaded, it is impossible at the level of a function's interface to know if a user passed in an actual character (a character literal or a variable of type char) or a numeric literal that just so happens to be small enough to fit into a char.

As such, when using char in an interface, one must be careful of accidental conflicts with what the user is providing.

Sequence container types (types that hold a number of elements in a sequence unrelated to the values of those elements) usually have a constructor that takes a count of Ts. This is the number of elements to create in the sequence, constructed via value initialization (there is also has a version that takes a T which is used to copy-initialize these elements, which is what you used).

But basic_string is special; it doesn't have such a constructor. If you tried to do std::string s(4);, you would get a compile error.

However, if you make the change you want, std::string s(4); would compile and execute. But it would not give you a sequence of 4 value-initialized characters. It would give you a string containing a single character with a value of 4. This is because the integer literal 4 can be converted to a char implicitly.

It's bad enough that basic_string is not consistent with the expectations of the common sequence container interface. But to actively make it compile but have radically different behavior would be way worse.

Furthermore, you can use list initialization to get what you want more explicitly:

std::string s1{some_char};
std::string s2 = {4}; //converts 4 to `char`
std::string s3 = {other_char};

List initialization is how we initialize containers of T with a sequence of Ts.

Related