Передача результатов `std::string::c_str()` в `mkdtemp()` с использованием `const_cast‹char*›()`

Хорошо, итак: мы все знаем, что использование const_cast<>() в любом месте настолько плохо, что это практически военное преступление в программировании. Так что это гипотетический вопрос о том, насколько плохо это может быть в конкретном случае.

А именно: я наткнулся на код, который делал что-то вроде этого:

std::string temporary = "/tmp/directory-XXXXX";
const char* dtemp = ::mkdtemp(const_cast<char*>(temporary.c_str()));
/// `temporary` is unused hereafter

… теперь я столкнулся с многочисленными описаниями того, как получить доступ для записи к базовому буферу экземпляра std::string (qv https://stackoverflow.com/a/15863513/298171 например) — все они имеют оговорку, что да, эти методы не гарантируют работу ни по одному стандарту C++, но на практике все они работают.

Имея это в виду, мне просто любопытно, как использование const_cast<char*>(string.c_str()) сравнивается с другими известными методами (например, вышеупомянутыми &string[0] и т. д.)… Я спрашиваю, потому что код, в котором я нашел этот метод, работает на практике, и я подумал Я бы посмотрел, что думают эксперты, прежде чем пытаться переписать неизбежную const_cast<>()-свободную версию.


person fish2000    schedule 07.12.2015    source источник
comment
Используйте std::vector<char> вместо std::string. У вас есть доступный для записи буфер без всего кастинга.   -  person PaulMcKenzie    schedule 07.12.2015
comment
Не то, что я прошу — я могу придумать кучу способов сделать это, у которых нет проблем. Меня интересует природа того, что делает проблему проблематичной.   -  person fish2000    schedule 07.12.2015
comment
Кстати, правильной реализацией будет следующая: char temporary[] = "/tmp/directory-XXXXX"; char* dtemp = mkdtemp(temporary); Так как это создает массив в стеке, который инициализируется копией строкового литерала, у вас есть все права изменять эту копию, включая передачу ее в mkdtemp(). Правда, здесь не используется блестящий материал C++, но он короче, проще и правильнее, чем тот вариант, с которым вы столкнулись :-)   -  person cmaster - reinstate monica    schedule 08.12.2015
comment
@cmaster абсолютно да — хотя это было бы более разумно и менее запутанно, фрагмент, который я разместил, является серьезным упрощением фактического производственного кода, который, естественно, достаточно запутан, чтобы сделать такое прямое редактирование непростым. Но вы, безусловно, правы в том, что делаете статическое назначение доступному для записи временному стеку как идеальному корму для mkdtemp() использования.   -  person fish2000    schedule 08.12.2015


Ответы (1)


  • const нельзя применить на аппаратном уровне, потому что на практике в негипотетической среде вы можете установить атрибут только для чтения только для полной страницы памяти размером 4 КБ, а на пути находятся огромные страницы, которые резко сокращают количество промахов ЦП при поиске в TLB.

  • const не влияет на генерацию кода, как это делает __restrict из C99. На самом деле const, грубо говоря, означает "отравить все попытки записи в эти данные, я хотел бы защитить здесь свои инварианты"

Поскольку std::string является изменяемой строкой, ее базовый буфер не может быть выделен в постоянной памяти. Таким образом, const_cast<> не должно вызывать здесь сбой программы, если только вы не собираетесь изменять некоторые байты за пределами базового буфера или пытаетесь что-то delete, free() или realloc(). Однако изменение символов в буфере можно классифицировать как нарушение инварианта. Поскольку после этого вы не используете экземпляр std::string, а просто выбрасываете его, это не должно спровоцировать сбой программы, если только какая-то конкретная реализация std::string не решит проверить целостность своих инвариантов перед уничтожением и вызвать сбой, если некоторые из них нарушены. Поскольку такая проверка не может быть выполнена менее чем за O(N) раз, а std::string является критичным для производительности классом, маловероятно, что кто-либо будет ее выполнять.

Другая проблема может возникнуть из-за стратегии Copy-on-Write. Таким образом, изменяя буфер напрямую, вы можете сломать другой экземпляр std::string, который разделяет буфер с вашей строкой. Но несколько лет назад большинство экспертов по C++ пришли к выводу, что COW слишком хрупок и слишком медленный, особенно в многопоточных средах, поэтому современные библиотеки C++ не должны его использовать, а вместо этого придерживаются конструкции перемещения, где это возможно, и избегают трафика кучи для небольших строки длины, где это применимо.

person Minor Threat    schedule 07.12.2015
comment
Ого, да, это объясняет, почему это работает в данном случае (насколько мне известно, передача в mkdtemp() заменяет символы, но ничего не перераспределяет). - person fish2000; 07.12.2015
comment
…или изменение символа на '\0'. - person emlai; 07.12.2015
comment
Ну, я думаю, что это немного вводит в заблуждение. Не разрешается отбрасывать const и затем записывать в буфер через этот указатель. Вы можете только читать. Чтобы получить доступ для записи к содержимому строки, используйте &s[0]. - person M.M; 07.12.2015
comment
@MM Верно, это было мое предположение, но, как я уже сказал, кажется, что в этом фрагменте все работает нормально, так что случайность компилятора (я использую последнюю версию clang++ FYI) или что-то еще, о чем я не знаю, что позволяет оба компиляция и выполнение здесь? - person fish2000; 07.12.2015
comment
@fish2000 вызывает неопределенное поведение, которое может включать в себя видимость работы по назначению. Или, другими словами, компиляторы могут поддерживать или не поддерживать его (и вы не получите никакого предупреждения, если они этого не сделают) - person M.M; 07.12.2015
comment
@ M.M Ахаааа, да, ужасный UB - спасибо, что разъяснили это. - person fish2000; 07.12.2015
comment
a) const нельзя применить на аппаратном уровне b) const не влияет на генерацию кода, как __restrict из C99. На самом деле const, грубо говоря, означает отравить все попытки записи в этот материал, надеюсь, это защитит мои инварианты. - person Minor Threat; 07.12.2015
comment
@MinorThreat… значит ли это, что const в корне является директивой компилятора и ничем иным? - person fish2000; 07.12.2015
comment
@MinorThreat Хорошо! Я действительно ценю детали, связанные с COW. Проголосовал бы еще раз, если бы мог, спасибо, доктор Угроза. - person fish2000; 07.12.2015
comment
Н.Б. То, о чем @MinorThreat говорит с общими буферами, будет более актуальным, когда мы закончим std::experimental::string_view — q.v. en.cppreference.com/w/cpp/experimental/basic_string_view - person fish2000; 07.12.2015
comment
О проверке инвариантов в деструкторе: std::string::size() определяется как сложность O(1), поэтому все реализации должны сохранять либо размер, либо указатель на конец строки. Этой информации достаточно, чтобы выполнить проверку правильности нулевого байта в конце строки за постоянное время. Таким образом, реализация может привести к сбою, если завершающий байт будет перезаписан. - person cmaster - reinstate monica; 08.12.2015
comment
@cmaster Я действительно считаю, что @MinorThreat гипотетически говорил о предполагаемой реализации std::string, которая дает сбой при разрушении, если ее инвариантные внутренние компоненты кажутся измененными каким-либо обнаруживаемым образом - однако наши соответствующие фактические std::string внутренние компоненты работают, они никому не приносят пользы, если они не могут быть уничтожены в способ, который а) непоколебимо экономичен и б) гарантирует noexcept … два качества, необходимые для того, чтобы std::string был повсеместно, в целом, полезно безопасным по всем направлениям. Проверка завершающего NUL не проверяет целостность и делает деструктор опасным. - person fish2000; 08.12.2015
comment
@ fish2000 Я вижу это так: завершающий байт NUL неявно помещается туда реализацией и не может быть удален легальным пользовательским кодом. Следовательно, если байт NUL перезаписан, программа уже ввела UB. Прерывание процесса — вполне допустимый образ действий в таком случае, а проверка стоит дешево. Да, это в значительной степени точка зрения языкового юриста, но я пришел к выводу, что лучше слушать языковых юристов, когда дело доходит до UB. Слишком много случаев, когда хорошо работавший код с треском ломался из-за того, что компилятор показал UB. - person cmaster - reinstate monica; 09.12.2015
comment
В общем, вы должны стремиться писать код, который работает по замыслу, а не случайно. Отбрасывая const здесь или переходя в страну неопределенного поведения, вы попадаете в режим работы по случайности. Позже компилятор обновляется и меняет реакцию на неопределенное поведение (они могут это сделать), и вы перекомпилируете свой код, и вдруг он иногда падает где-то странным образом. Просто избегайте неопределенного поведения и заставьте свой код работать по замыслу, а не случайно. - person legalize; 11.12.2015
comment
@legalize: Конечно, мы должны сделать все возможное, чтобы избежать UB. Но @fish2000 спросил, что именно может произойти. const не говорит, что данные вообще нельзя изменить. В нем говорится, что у вас нет надлежащего интерфейса для изменения этих данных в этой конкретной области. Но есть разные области видимости, и компилятор в общем случае не может их все отследить. - person Minor Threat; 11.12.2015
comment
@MinorThreat Поскольку мы вступаем в область неопределенного поведения, ответом на то, что может произойти, является буквально все, что захочет сделать компилятор, включая вызов игры hack или ханойских башен. - person legalize; 11.12.2015
comment
@legalize: я видел несколько случаев, когда Microsoft полагалась на UB. Преобразователи кода ATL/WTL в стеке, преобразования типов через unions, void(*)(LPRECT) -> void(*)(const CRect&) и т.д. и т.п. - person Minor Threat; 11.12.2015