Почему формат C 20 не имеет строгого определения типа для строки формата?

#c #c 20 #fmt

#c #c 20 #fmt

Вопрос:

C 20 вводит следующую функцию форматирования (версии locale и wstring_view игнорируются, поскольку они не влияют на вопрос):

 template<class... Args>
std::string format(std::string_view fmt, const Argsamp;... args);
 

В этом нет ничего плохого, но мне интересно, почему нет перегрузки, которая принимает «strong typedef», что-то вроде

 template<class... Args>
    std::string format(std::format_string fmt, const Argsamp;... args);
 

Мои предположения были бы некоторыми или всеми из следующих:

  1. повышенная сложность реализации
  2. увеличенное время компиляции
  3. раздувание кода

, но мне интересно, обсуждалось ли это когда-либо во время стандартизации.

Комментарии:

1. Поскольку "Bob" , несомненно, будет действительным format_string в соответствии с вашим предложением и "Hi, {}" является действительным std::string , единственный способ, которым я мог бы увидеть, что это работает, — это отключить первую перегрузку и заставить людей приводить свои строки формата std::format_string к вызову format . Я полагаю, вы могли бы возразить, что вызов std::format с большим количеством аргументов, чем принимает строка формата, должен быть ошибкой, но это все равно будет ошибкой во время выполнения при типичных сценариях использования.

2. Итак, что именно "Bob" помешало бы преобразованию в a std::format_string таким образом, который std::format("Bob", "Hi, {}") запрещен?

3.Если вы полагаетесь на явные конструкторы для предотвращения std::format("Bob", "Hi, {}") вызова, это означает, что вы отключаете перегрузку std::format , которая принимает an std::string_view в качестве первого аргумента. В этот момент ни std::format("Bob", "Hi, {}") один из nor std::format("Hi, {}", "Bob") не является допустимым вызовом, потому что ни один из них не имеет a std::format_string в качестве первого аргумента. Но если бы вы на самом деле сохранили их в переменных std::format_string fs("Hello {}"); std::string name("Bob"); std::format(name, fs) , это не сработало бы?

4. Если ваша цель не std::format("Bob", "Hello, {}") состоит в том, чтобы предотвратить компиляцию, довольно сложно понять, что это на самом деле делает. Если проблема в том, что у вас есть собственные функции, которые в настоящее время имеют подпись foo(std::string, std::string) , и вы хотите переписать их, foo(format_string, std::string) чтобы попытаться обеспечить соблюдение порядка, вы можете написать свою собственную тонкую оболочку std::string .

5. @NathanPierson: » Я хочу, чтобы пользователи могли избегать компиляции подобного кода в своем коде (если они принудительно используют format_string )». Но то, что вы предлагаете, этого не сделает, потому что ни один из них не является format_string s . Это строковые литералы.

Ответ №1:

Смысл строгих typedefs в том, чтобы не допустить, чтобы это сработало:

 void takes_id(SomeIdType);
takes_id(42); 
 

Смысл format в том, чтобы позволить этому работать:

 format("User {} owes me {} points.", name, 100);
 

Это строковый литерал. Требование строгого типа означает большую нагрузку на пользователей, необходимость писать что-то вроде этого:

 format(format_string("User {} owes me {} points."), name, 100);
 

Это не является бременем для типичного варианта использования strong typedef, поскольку вы фактически будете торговать SomeIdType s. У вас будет функция, которая выдает вам SomeIdType , вы будете хранить член типа SomeIdType . В принципе, количество фактических преобразований будет довольно минимальным … поэтому на сайте вызова вы просто пишете takes_id(my_id) , и код в основном выглядит так же, с дополнительной безопасностью.

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

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

 format(name, "User {} owes me {} points.", 100);
 

Или даже:

 format(name, 100);
 

Первое, похоже, вряд ли когда-либо произойдет. Последнее, безусловно, возможно, если первый аргумент достаточно похож на строку. Но является ли это достаточно распространенной проблемой, чтобы заставить всех писать больше кода? Я так не думаю.

Теперь, если бы строковые литералы имели свой собственный отдельный тип from const char[N] (и я действительно хотел бы, чтобы они это сделали), тогда можно было бы создать тип, который неявно std::string_literal конструируется из, но должен быть явно сконструирован из std::string_view . И если бы это было так, то API, вероятно, использовал бы это — поскольку в обычном случае для этого не потребовалось бы аннотации, а не использование строковых литералов кажется достаточно редким, чтобы требовать явного приведения, кажется … нормально?

Кроме того, по вопросу безопасности проблема заключается не столько в передаче строки неправильного типа, сколько в фактической возможности проверки ее значения в ее контексте:

 format("User {} owes me {} points.", name);
 

Нам бы очень хотелось, чтобы это не компилировалось, хотя мы указали строку формата в правильном месте. И, по-видимому, это возможно сделать. Но для этого нам также не нужны строгие определения типа, нам просто нужна возможность узнать, является ли строка формата постоянным выражением или нет.


Подводя итог, можно сказать, что ответ на:

но мне интересно, почему нет перегрузки, которая принимает «strong typedef»

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

Комментарии:

1. хороший ответ, но просто для пояснения, поскольку мой вопрос, вероятно, недостаточно ясен в этом вопросе: я не хочу удалять перегрузку string_view, мне просто интересно, почему перегрузка format_string не существует.

2. @NoSenseEtAl Если бы перегрузка format_string действительно существовала, это не помешало бы кому-либо вызывать перегрузку string_view.

3.Пытался получить супер приятную ссылку на твиттер, подтвержденную godbolt, не уверен, что у меня не получается с определением макросов, или версия godbolt «trunk» слишком старая … godbolt.org/z/jxPfhz github.com/fmtlib/fmt/search?l=C++amp;q=VERSION

4. @NoSenseEtAl В самом начале я спросил вас, имеете ли вы в виду проверку достоверности аргумента времени во время компиляции, которую вы можете выполнять в fmt библиотеке, и вы сказали, что нет, вы говорили о чем-то другом. И соблюдайте ограничения этих проверок: godbolt.org/z/bd5oxc это совершенно удобно для компиляции, даже несмотря на то, что, очевидно, нет способа узнать во время компиляции, что fmt_string будет действительным.

5. @NathanPierson Я отвечал на ссылку Барри, указанную в его ответе… это не имеет прямого отношения к вопросу.