Несоответствие между std::string и строковыми литералами

Я обнаружил тревожную несогласованность между std::string и строковыми литералами в С++ 0x:

#include <iostream>
#include <string>

int main()
{
    int i = 0;
    for (auto e : "hello")
        ++i;
    std::cout << "Number of elements: " << i << '\n';

    i = 0;
    for (auto e : std::string("hello"))
        ++i;
    std::cout << "Number of elements: " << i << '\n';

    return 0;
}

Вывод:

Number of elements: 6
Number of elements: 5

Я понимаю механику, почему это происходит: строковый литерал - это действительно массив символов, который включает нулевой символ, и когда цикл на основе цикла вызывает std::end() в массиве символов, он получает указатель мимо конец массива; поскольку нулевой символ является частью массива, он, таким образом, получает указатель за нулевым символом.

Однако, я думаю, что это очень нежелательно: неужели std::string и строковые литералы должны вести себя одинаково, когда дело касается свойств как основных, как их длина?

Есть ли способ разрешить эту несогласованность? Например, могут ли быть перегружены std::begin() и std::end() для массивов символов, так что диапазон, который они ограничивают, не включает завершающий нулевой символ? Если да, почему это не было сделано?

EDIT. Чтобы оправдать мое негодование немного больше тем, кто сказал, что я просто страдаю последствиями использования строк стиля С, которые являются "устаревшей функцией", рассмотрите код, подобный следующее:

template <typename Range>
void f(Range&& r)
{
    for (auto e : r)
    {
        ...
    }
}

Ожидаете ли вы, что f("hello") и f(std::string("hello")) сделать что-то другое?

Ответ 1

Несоответствие может быть разрешено с помощью другого инструмента в С++ 0x toolbox: пользовательских литералов. Использование соответствующего заданного пользователем литерала:

std::string operator""s(const char* p, size_t n)
{
    return string(p, n);
}

Мы сможем написать:

int i = 0;     
for (auto e : "hello"s)         
    ++i;     
std::cout << "Number of elements: " << i << '\n';

Теперь выводится ожидаемое число:

Number of elements: 5

С этими новыми литералами std::string, возможно, нет больше причин использовать строковые литералы C-стиля, когда-либо.

Ответ 2

Если мы перегрузили std::begin() и std::end() для массивов const char, чтобы вернуть меньше, чем размер массива, тогда следующий код будет выводить 4 вместо ожидаемого 5:

#include <iostream>

int main()
{
    const char s[5] = {'h', 'e', 'l', 'l', 'o'};
    int i = 0;
    for (auto e : s)
        ++i;
    std::cout << "Number of elements: " << i << '\n';
}

Ответ 3

Однако, я думаю, что это очень нежелательно: неужели std::string и строковые литералы должны вести себя одинаково, когда дело касается свойств как основных, как их длина?

Строковые литералы по определению имеют (скрытый) нулевой символ в конце строки. Std:: строки нет. Поскольку std:: strings имеют длину, этот нулевой символ немного лишний. Стандартный раздел в библиотеке строк явно допускает ненулевые завершенные строки.

Edit
Я не думаю, что я когда-либо давал более спорный ответ в смысле огромного количества upvotes и огромного количества downvotes.

Итератор auto, применяемый к массиву C-стиля, выполняет итерацию по каждому элементу массива. Определение диапазона производится во время компиляции, а не во время выполнения. Это плохо сформировано, например:

char * str;
for (auto c : str) {
   do_something_with (c);
}

Некоторые люди используют массивы типа char для хранения произвольных данных. Да, это стиль мышления старого стиля C, и, возможно, они должны были использовать std:: array в стиле С++, но конструкция вполне допустима и весьма полезна. Эти люди были бы весьма расстроены, если бы их автоматический итератор над char buffer[1024]; остановился на элементе 15 только потому, что этот элемент имеет то же значение, что и нулевой символ. Автоматический итератор над Type buffer[1024]; будет работать до конца. Что делает массив char настолько достойным совершенно другой реализации?

Обратите внимание, что если вы хотите, чтобы автоматический итератор поверх символьного массива останавливался раньше, есть простой механизм для этого: добавьте оператор if (c == '0') break; в тело вашего цикла.

Итог: здесь нет несогласованности. Итератор auto над массивом char [] согласуется с тем, как автоматический итератор работает с любым другим массивом C-стиля.

Ответ 4

То, что вы получаете 6, в первом случае - это абстракция, которую невозможно избежать в C. std::string "fixes". Для совместимости поведение строковых литералов C-стиля в С++ не изменяется.

Например, можно ли перегрузить std:: begin() и std:: end() для массивы символов, так что диапазон, который они ограничивают, не включает завершающий нулевой символ? Если да, почему это не было сделано?

Предполагая доступ через указатель (в отличие от char[N]), только путем встраивания переменной внутри строки, содержащей количество символов, так что поиск NULL больше не требуется. К сожалению! Это std::string.

Способ "разрешить несогласованность" заключается не в использовании устаревших функций вообще.

Ответ 5

В соответствии с N3290 6.5.4, если диапазон является массивом, граничные значения автоматически инициализируется без отправки begin/end функции. Итак, как насчет подготовки некоторой обертки, как показано ниже?

struct literal_t {
    char const *b, *e;
    literal_t( char const* b, char const* e ) : b( b ), e( e ) {}
    char const* begin() const { return b; }
    char const* end  () const { return e; }
};

template< int N >
literal_t literal( char const (&a)[N] ) {
    return literal_t( a, a + N - 1 );
};

Тогда будет действовать следующий код:

for (auto e : literal("hello")) ...

Если ваш компилятор предоставляет пользовательский литерал, это может помочь сокращать:

literal operator"" _l( char const* p, std::size_t l ) {
    return literal_t( p, p + l ); // l excludes '\0'
}

for (auto e : "hello"_l) ...

РЕДАКТИРОВАТЬ:. Ниже перечислены меньшие накладные расходы (пользовательский литерал не будет доступен, хотя).

template< size_t N >
char const (&literal( char const (&x)[ N ] ))[ N - 1 ] {
    return (char const(&)[ N - 1 ]) x;
}

for (auto e : literal("hello")) ...

Ответ 6

Если вам нужна длина, вы должны использовать strlen() для строки C и .length() для строки С++. Вы не можете обрабатывать строки C и строки С++ одинаково - они имеют другое поведение.