Почему нет неявного преобразования из std::string_view в std::string?

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

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

Прекрасно, что в С++ есть замена необработанному указателю const char*, но при этом он становится супер запутанным и урезанным до костей:

  • Неявный const char* -> std::string: ОК
  • Неявный std::string_view -> std::string: НЕТ
  • Назначение std::string = const char* : ОК
  • Назначение std::string = std::string_view: ОК
  • Добавление std::string += const char* : ОК
  • Добавление std::string += std::string_view: ОК
  • Объединение const char* + std::string: ОК
  • Объединение std::string_view + std::string: НЕТ
  • Объединение std::string + const char*: ОК
  • Объединение std::string + std::string_view: НЕТ

Я что-то пропустил или это полная ерунда?

В конце концов, насколько полезно это строковое представление без всех важных частей, которые делают его похожим на const char*? Какой смысл интегрировать его в экосистему stdlib, не делая при этом последнего шага к его завершению? В конце концов, если нам нужен объект, представляющий часть строки, мы можем написать свой собственный. На самом деле многие библиотеки уже сделали это много лет назад. Весь смысл создания чего-то стандартного в том, чтобы сделать его полезным для самых разных вариантов использования, не так ли?

Собираются ли они исправить это в C++23?


person GreenScape    schedule 28.11.2017    source источник
comment
Я голосую за то, чтобы закрыть этот вопрос как не по теме, потому что это не вопрос, это разглагольствование.   -  person Barry    schedule 28.11.2017
comment
Это, вероятно, относится к reddit.com/r/cpp (некоторые члены комитета C++ читали это).   -  person Nikos C.    schedule 30.11.2017
comment
Кажется несколько несправедливым, что этот вопрос, обсуждение которого помогло бы многим программистам, был закрыт.   -  person lrleon    schedule 17.06.2018
comment
@ Барри, это действительно вопрос. Это также разглагольствование, но это не делает его не вопросом. ОП выражает законный шок, вполне понятный ИМО, что нисколько не умаляет технической законности и полезности вопроса. Было бы здорово, если бы вопрос можно было возобновить.   -  person Don Hatch    schedule 18.07.2019
comment
Серьезно, что не так? Звучит как вопрос для меня. ;)   -  person Innocent Bystander    schedule 21.12.2019


Ответы (2)


Проблема в том, что std::string_view -> std::string создает копию базовой памяти с выделением кучи, тогда как неявный std::string -> std::string_view этого не делает. Если вы удосужились использовать std::string_view в первую очередь, то вы, очевидно, заботитесь о копиях, поэтому вы не хотите, чтобы это происходило неявно.

Рассмотрим этот пример:

void foo1(const std::string& x)
{
    foo2(x);
}
void foo2(std::string_view x)
{
    foo3(x);
}
void foo3(const std::string& x)
{
    // Use x...
}

Функция foo2 могла бы использовать параметр const std::string&, но использовала std::string_view, так что она более эффективна, если вы передаете строку, которая не является std::string; там никаких сюрпризов. Но это менее эффективно, чем если бы вы просто дали ему параметр const std::string&!

  • Когда foo2 вызывается с аргументом std::string (например, foo1): Когда foo2 вызывает foo3, создается копия строки. Если бы у него был аргумент const std::string&, он мог бы использовать объект, который у него уже был.
  • Когда foo2 вызывается с аргументом const char*: std::string копия должна быть сделана рано или поздно; с параметром const std::string& он создается раньше, но в целом в любом случае есть ровно одна копия.

Теперь представьте, что foo2 вызывает несколько функций, таких как foo3, или вызывает foo3 в цикле; он снова и снова создает один и тот же объект std::string. Вы бы хотели, чтобы компилятор уведомил вас об этом.

person Arthur Tacca    schedule 28.11.2017
comment
По моему опыту, чаще всего std::string_view используется для оптимизации синтаксического анализа строки. Результат парсинга, очевидно, нельзя хранить в представлении, поэтому его следует перенести в постоянное хранилище (std::string). Почему это должно быть так многословно? В конце концов, я все контролирую, и я должен знать, где представление может конвертироваться. - person GreenScape; 28.11.2017
comment
То, что вы описываете, это плохой дизайн. Плохой дизайн другого фрагмента кода не должен быть причиной того, что дизайн std::string_view тоже плохой. Кроме того, программист, видящий foo1() и foo3(), должен знать о конверсиях. А если все это проигнорировать, то как явные подсказки в вашем примере? Из-за плохого дизайна программист все равно вынужден создавать std::string, не так ли? - person GreenScape; 28.11.2017
comment
@GreenScape Я не понимаю смысла вашего первого комментария; может быть, вы могли бы добавить пример кода на свой вопрос? Я также не очень понимаю ваш второй пункт; что такое плохой дизайн? Тот факт, что foo3 принимает const std::string&? Это было чрезвычайно распространено до появления std::string_view, и вокруг лежит много этого кода. Или реализация foo2? Потому что я согласен (хотя это плохая реализация, а не плохой дизайн), но это может легко произойти случайно, и вы бы хотели, чтобы компилятор это уловил. - person Arthur Tacca; 28.11.2017
comment
Просто, чтобы обратиться к последней части вашего второго комментария (извините, что я пропустил это изначально): если вы получите ошибку компилятора в функции, которая потребует std::string несколько раз, вы можете решить создать ее один раз в начале функции и повторно использовать это для нескольких вызовов функций. Или вы можете изменить сигнатуру функции, чтобы она принимала const std::string&. - person Arthur Tacca; 28.11.2017
comment
@ArthurTacca, что бы вы сказали, если бы в вашем примере string_view было заменено на const char*? уже несколько десятилетий мы имеем дело с неявным преобразованием const char* -> string. string_view помогает манипулировать временной const char* памятью, очевидно, что в какой-то момент string_view инициирует выделение путем преобразования в строку, и пользователь несет ответственность за то, где именно. Более того, старый код, созданный с помощью const std::string& везде, не мог легко транслироваться с помощью string_view из-за отсутствия неявного преобразования. - person Juicebox; 08.04.2020
comment
@Juicebox Это совершенно другой сценарий. Во-первых, строковые литералы — это const char*, поэтому должно быть неявное преобразование, чтобы f("foo") работало, когда f принимает std::string (в противном случае на улице начались бы беспорядки). Во-вторых, вы используете string_view только в том случае, если вы специально пытаетесь избежать выделения памяти - в этом весь смысл моего ответа, но это, конечно, не весь смысл const char*. - person Arthur Tacca; 06.05.2020
comment
@Juicebox очевидно, что в какой-то момент представление string_view вызовет выделение путем преобразования в строку. Да, очевидно, что преобразование в string вызовет выделение, но существует неявное преобразование, тогда не очевидно, когда это преобразование происходит. Компилятор, а не пользователь, несет большую ответственность за обнаружение возможных ошибок. И действительно ли проблема написать f(string(x)) вместо f(x), если вы хотите сделать преобразование? - person Arthur Tacca; 06.05.2020
comment
Это было бы гораздо менее раздражающим, если бы они предоставили какой-то простой способ явного преобразования. - person NO_NAME; 08.05.2020
comment
@NO_NAME Как было сказано в моем последнем комментарии, если f принимает std::string, а x является std::string_view, то вы можете просто написать f(std::string(x)). Я не могу представить себе ничего явного, что могло бы быть проще этого. О чем ты думал? - person Arthur Tacca; 08.05.2020
comment
@ArthurTacca Извините, я не знал, что универсальный конструктор работает для std::string_view. - person NO_NAME; 08.05.2020
comment
Вы говорите, что передача std::string_view менее эффективна, чем использование const std::string &. Этот тест наглядно показывает обратное. Какие условия сделают ваше утверждение верным? - person psimpson; 09.05.2020
comment
@psimpson В вашем комментарии много ошибок (а) я сказал, что string_view более эффективен; (б) ответ на какие условия...? находится в том же предложении из моего ответа: когда вы передаете строку, которая не является std::string (потому что она избегает копирования); (c) в этом микробенчмарке у вас действительно есть std::string, так что разницы быть не должно; тест явно фальшивый, потому что вывод текста на консоль, даже простое копирование в буфер для последующего вывода, намного дороже, чем разница между этими типами параметров, когда вы начинаете с std::string. - person Arthur Tacca; 10.05.2020
comment
Вероятно, это было искажение, но в вашем сообщении говорится, что после блока кода это менее эффективно, чем если бы вы просто дали ему параметр const std::string& !. Возможно, эту строку следует удалить или отредактировать, если она предназначалась для ссылки, возможно, на std::string. И да, я обычно не провожу такого рода бенчмаркинг, поэтому ваш ответ заставил меня заподозрить, что они могли выбрать плохой тест. На самом деле вы оба пытаетесь сказать одно и то же. Я ценю ваш ответ. - person psimpson; 10.05.2020
comment
@psimpson Ах, извините, да, я также говорю, что string_view менее эффективен, чем const std::string&, но только в конкретной ситуации, когда string_view находится в середине бутерброда const std::string&, показанного во фрагменте кода. Это ясно, если вы прочитаете это предложение в контексте с одним или двумя предложениями заранее. Более того, такая ситуация не может произойти на практике без явного приведения (или ошибки компилятора!), потому что нет неявного приведения — помните, об этом был первоначальный вопрос, и этот пример призван показать, почему отсутствие неявного приведения актерский состав - это хорошо. - person Arthur Tacca; 10.05.2020
comment
Я ДЕЙСТВИТЕЛЬНО прочитал это вне контекста, вероятно, потому, что я задавал другой вопрос. Неэффективность заключается в foo2::foo3, а не в foo1::foo2, о чем мне действительно нужно подумать, применяя его прямо сейчас. Благодарю вас! - person psimpson; 10.05.2020
comment
Из этих пяти заданий три работают, а два нет. Новичку очень сложно понять, почему это так (v всегда std::string_view): std::string s1(v); (хорошо), std::string s2{v}; (хорошо), std::string s3 = v; (не получается), std::string s4; s4 = v; (хорошо), struct A { std::string a; }; A a { v }; (не получается). Первый и третий случай выглядят одинаково, так почему же один работает, а другой нет? Корпус номер 3 и 4 тоже выглядят похоже, но работает только один. И то же самое верно для случаев 2 и 5. Я очень хорошо понимаю разглагольствования открывателя темы. - person Kai Petzke; 02.11.2020
comment
@KaiPetzke Я определенно согласен с тем, что эти примеры проблематичны. Они особенно неприятны, потому что связаны с инициализацией, которая печально известна в C++. Я никогда не утверждал, что каждый аспект интерфейса string_view идеален, только то, что проблема, которую я описал, настолько серьезна, что ее исправление путем явного конструктора перевешивает удобство оставления его неявным. Сделать каждый аспект string_view чистым просто невозможно в C++. Возможно, аналогичный класс можно было бы чисто реализовать в Rust, особенно благодаря его управлению временем жизни. - person Arthur Tacca; 03.11.2020

Потому что дорогие неявные преобразования нежелательны...

Вы указали только одно неявное преобразование из всего набора примеров: const char* -› std::string; и вы просите еще один. Во всех остальных функциях, помеченных 'OK', выделение памяти очевидно/явно:

  • Назначения: когда вы назначаете что-либо объекту-владельцу с переменным размером хранилища, подразумевается, что может потребоваться выделение. (Если это не задание на перемещение, но неважно.)
  • Приложение I: когда вы добавляете к объекту-владельцу переменный размер хранилища, становится еще более очевидным, что для размещения дополнительных данных потребуется выделение. (У него может быть достаточно зарезервированного места, но никто не гарантирует этого в отношении строк.)
  • Конкатенации: во всех трех случаях память выделяется только для результата, а не для какого-либо промежуточного объекта. Выделение для результата, очевидно, необходимо, поскольку операнды конкатенации остаются нетронутыми (и в любом случае нельзя предполагать, что они содержат результат).

Неявные преобразования в целом имеют свои преимущества и недостатки. С++ на самом деле в основном скуп на них и наследует большинство неявных преобразований от C. Но - const char* в std::string является исключением. Как отмечает @ArthurTacca, он выделяет память. Основные рекомендации C++ гласят:

C.164: избегайте неявных операторов преобразования

Причина.
Неявные преобразования могут быть необходимы (например, double в int), но часто вызывают неожиданности (например, String в строку в стиле C).

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


PS - у std::string_view довольно много ошибок; см. это выступление Виктора Чиуры на CppCon 2018:

Достаточно string_view, чтобы повеситься

Так что помните, что это не какая-то универсальная панацея; это еще один класс, который нужно использовать осторожно, а не небрежно.

... явного конструктора достаточно.

действительно существует явный конструктор std::string из std::string_view. Используйте его, передав std::string{my_string_view} функции, принимающей строку.

person einpoklum    schedule 19.06.2020
comment
Почему? Это такая беда? Каков наихудший сценарий? Дополнительное выделение? При необходимости его можно легко профилировать и оптимизировать. С другой стороны, у нас несовершенный API. API для инвалидов не так просто обойти. Такой API отвратительно использовать: тонны шаблонов только для того, чтобы избежать этого «таинственного катастрофического неявного преобразования». И помните, мы говорим о необычном типе. У этого есть гораздо более серьезная проблема: оборванные ссылки. И мы в порядке с этим. Но не с неявным выделением, о нет! - person GreenScape; 22.06.2020
comment
@GreenScape: мы говорим о том, что происходит по умолчанию, а не о том, что вообще разрешено. Кроме того, зачем подкрадываться к дополнительному распределению, о котором программист не просил? Однако... это заставляет меня кое о чем задуматься... - person einpoklum; 22.06.2020
comment
@GreenScape: Почему бы вам просто не использовать явный конструктор? - person einpoklum; 22.06.2020
comment
Как я писал в своем первоначальном комментарии: это шаблон. Я ничего не знаю о вашем опыте, но согласно моему, если вы хотите эффективно использовать string_view, вы будете использовать его везде. Это означает, что вы собираетесь использовать явное std::string{foo} везде. Это ненужный шаблон, и его не стоит продавать только для того, чтобы избежать случайных неявных преобразований. Это того не стоит! - person GreenScape; 23.06.2020
comment
Это означает, что вы собираетесь использовать явный std::string{foo} везде - если это так, то вам, вероятно, не следует использовать string_view в первую очередь. Тогда просто используйте строки. Большая часть смысла использования string_view в первую очередь заключается в том, чтобы избежать этих распределений. - person einpoklum; 23.06.2020
comment
В этом суть. Сделайте свой API как можно более легким (string_view), а затем позвольте клиентам решать, как его использовать. Увы, принятие решения в пользу string_view вынуждает клиентов использовать шаблоны. Пример: у вас есть постоянный объект HTTP-запроса. Чтобы избежать распределения, вы можете проиндексировать его с помощью string_view и позволить клиентам извлекать данные как: std::optional<std::string_view> find_header(...). Это позволяет хранить весь большой двоичный объект в одном буфере. Это удобно для распределения, удобно для перемещения, но не для клиентов. - person GreenScape; 23.06.2020
comment
@GreenScape, я полностью с тобой согласен. Я решил заменить std::string const & на string_view для всех возможных аргументов функций в моих библиотеках, и в итоге это стоило мне буквально нескольких дней работы по шаблонизации моего кода, чтобы исправить все внезапные ошибки неявного преобразования (которые были желательными преобразованиями в контекст моей программы). Это заставило меня задуматься о том, что комитет ошибается в этом вопросе, потому что его роль не в том, чтобы покровительствовать тому, как я кодирую. - person Jango; 27.07.2020