Должен ли я передавать в unsigned char перед вызовом toupper(), tolower(), et al.?

Некоторое время назад кто-то с высокой репутацией здесь, в Кару, написал в комментарии, что необходимо наложить char -argument на unsigned char перед вызовом std::toupper и std::tolower (и аналогичных функций).

С другой стороны, Бьярн Страуструп не упоминает об этом на языке программирования C++. Он просто использует toupper как

string name = "Niels Stroustrup";

void m3() {
  string s = name.substr(6,10);  // s = "Stroustr up"
  name.replace(0,5,"nicholas");  // name becomes "nicholas Stroustrup"
  name[0] = toupper(name[0]);   // name becomes "Nicholas Stroustrup"
} 

(Цитируется из упомянутой книги, 4-е издание.)

Ссылка говорит о том, что входные данные должны быть представлены как unsigned char. Для меня это звучит так, как будто это верно для каждого char, поскольку char и unsigned char имеют одинаковый размер.

Так что этот актёрский состав ненужен или Страуструп небрежен?

Редактировать: руководство libstd C++ упоминает, что вводимый символ должен быть из базового исходного набора символов, но не приводится. Я предполагаю, что это покрыто ответом @Keith Thompson, все они имеют положительное представление как signed char и unsigned char?

Ответ 1

Да, аргумент toupper должен быть преобразован в unsigned char, чтобы избежать риска поведения undefined.

Типы char, signed char и unsigned char являются тремя различными типами. char имеет тот же диапазон и представление, что и signed char или unsigned char. (Обычная char очень часто подписывается и может представлять значения в диапазоне -128.. + 127.)

Функция toupper принимает аргумент int и возвращает результат int. Цитируя стандарт C, раздел 7.4, пункт 1:

Во всех случаях аргумент int, значение которого должно быть представляемый как unsigned char или должен равняться значению макрос EOF. Если аргумент имеет любое другое значение, поведение undefined.

(С++ включает большую часть стандартной библиотеки C и отсылает его определение к стандарту C.)

Оператор индексирования [] на std::string возвращает значение char. Если plain char является подписанным типом, и если значение, возвращаемое name[0], оказывается отрицательным, то выражение

toupper(name[0])

имеет поведение undefined.

Язык гарантирует, что даже если plain char подписан, все члены базового набора символов имеют неотрицательные значения, поэтому при инициализации

string name = "Niels Stroustrup";

программа не рискует поведением undefined. Но да, в общем случае значение char, переданное в toupper (или любой из функций, объявленных в <cctype>/<ctype.h>, должно быть преобразовано в unsigned char, так что неявное преобразование в int выиграло 't дает отрицательное значение и вызывает поведение undefined.

Функции <ctype.h> обычно реализуются с использованием справочной таблицы. Что-то вроде:

// assume plain char is signed
char c = -2;
c = toupper(c); // undefined behavior

может индексироваться за пределами этой таблицы.

Обратите внимание, что преобразование в unsigned:

char c = -2;
c = toupper((unsigned)c); // undefined behavior

не позволяет избежать проблемы. Если int - 32 бита, преобразование значения char -2 в unsigned дает 4294967294. Затем он неявно преобразуется в int (тип параметра), который, вероятно, дает -2.

toupper может быть реализован так, что он ведет себя разумно для отрицательных значений (принимая все значения от CHAR_MIN до UCHAR_MAX), но этого не требуется. Кроме того, функции в <ctype.h> должны принимать аргумент со значением EOF, который обычно -1.

Стандарт С++ корректирует некоторые стандартные функции библиотеки C. Например, strchr и несколько других функций заменяются перегруженными версиями, которые обеспечивают правильность const. Нет таких настроек для функций, объявленных в <cctype>.

Ответ 2

Ссылка ссылается на значение, представляемое как unsigned char, а не на unsigned char. То есть, поведение не определено, если фактическое значение не находится между 0 и UCHAR_MAX (обычно 255). (Или EOF, что в основном является причиной, когда вместо char используется int).

Ответ 3

В C, toupper (и многие другие функции) возьмите int, даже если вы ожидаете, что они возьмут char s. Кроме того, char подписывается на некоторых платформах и без знака на других.

Совет при нажатии unsigned char перед вызовом toupper верен для C. Я не думаю, что он нужен в С++, если вы передадите ему int в диапазоне. Я не могу найти ничего конкретного, нужно ли ему в С++.

Если вы хотите обойти проблему, используйте toupper, определенный в <locale>. Это шаблон и принимает любой приемлемый тип символа. Вы также должны передать его std::locale. Если вы не знаете, какой язык выбрать, используйте std::locale(""), который должен быть предпочтительным для пользователя пользователем:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <locale>
#include <string>

int main()
{
    std::string name("Bjarne Stroustrup");
    std::string uppercase;

    std::locale loc("");

    std::transform(name.begin(), name.end(), std::back_inserter(uppercase),
                   [&loc](char c) { return std::toupper(c, loc); });

    std::cout << name << '\n' << uppercase << '\n';
    return 0;
}

Ответ 4

К сожалению, Страуструп был невнимателен:-( И да, латинские коды букв должны быть неотрицательными (и не требуется литье)...
Некоторые реализации корректно работают без кастинга без знака char...
По опыту, это может стоить несколько часов, чтобы найти причину segfault такого toupper (когда известно, что существует segfault)...
И есть также isupper, islower и т.д.

Ответ 5

Вместо того, чтобы отбрасывать аргумент как unsigned char, вы можете использовать функцию. Вам нужно будет включить функциональный заголовок. Здесь пример кода:

#include <string>
#include <algorithm>
#include <functional>
#include <locale>
#include <iostream>

int main()
{
    typedef unsigned char BYTE; // just in case

    std::string name("Daniel Brühl"); // used this name for its non-ascii character!

    std::transform(name.begin(), name.end(), name.begin(),
            (std::function<int(BYTE)>)::toupper);

    std::cout << "uppercase name: " << name << '\n';
    return 0;
}

Вывод:

uppercase name: DANIEL BRüHL

Как и ожидалось, toupper не влияет на символы без ascii. Но это литье полезно для предотвращения неожиданного поведения.