Open Neargye opened 3 years ago
Игорь Гусаров 23 января 2020, 14:13
Обратная совместимость. Любое предложение, которое ломает существующий код (даже с самыми благими намерениями!), с большой вероятностью будет отклонено. Для выдвижения ломающих предложений нужно, чтобы был показан колоссальный выигрыш от них. Боюсь, что в данном случае выигрыш звучит не очень убедительно: "защититься от возможных ошибок кодирования, от которых и так можно защититься имеющимися в языке средствами".
Унификация. С точки зрения generic programming лучше, чтобы все типы вели себя по возможности единообразно. Например, есть предложение P0146R1 Regular Void. Оно нацелено на унификацию войда с остальными типами, чтобы в шаблонном коде можно было написать "auto x = foo();" или "promise.set_value(foo());" даже в случае, когда foo возвращает void. Это предложение рассматривается комитетом и уже дошло до стадии пробной реализации.
А тут предлагается ухудшить единообразие типов, лишив группу типов возможности выступать в качестве аргумента функции.
*Никита Колотов 26 января 2020, 21:40 Игорь Гусаров, Мне кажется, что вы как-то не так поняли смысл этого предложения. На единообразие типов оно влияет только положительно, так как направлено на отмену специального правила для интерпретации типов аргументов фунции и переход к единообразным правилам на этот счет для всех типов. Заметьте, что оно затрагивает написание сигнатуры функции, т.е. для выражений "auto x = foo();" или "promise.set_value(foo());" оно вообще не применяется.
Игорь Гусаров 27 января 2020, 11:53 Никита Колотов, возможно, я действительно как-то не так понял. Вы говорите о единообразном описании типов в стандарте?
А я говорю уже о следующем шаге: как это предложение отразится на единообразии сценариев использования языка:
using T1 = int; // Built-in integer type
using T2 = MyStruct; // User-defined complete type
using T3 = void (*)(); // Pointer to function
using T4 = void (&)(); // Reference to function
using T5 = void (); // Function
using T6 = void; // Void
void foo1(T1 arg); // 1
void foo2(T2 arg); // 2
void foo3(T3 arg); // 3
void foo4(T4 arg); // 4
void foo5(T5 arg); // 5
void foo6(T6 arg); // 6
В настоящий момент валидными декларациями являются строки 1-5.
Рассматриваемое предложение предлагает сделать строку 5 невалидной, тем самым уменьшая количество допустимых способов использования типа T5, и усложняя разработку обобщённого кода:
template <typename T>
struct Processor
{
void Do(T arg);
};
// With the proposal in effect,
// the code above would fail for T = T5.
// Hence, need special implementation
// for function types...
template <>
struct Processor<T5>
{
void Do(T5& arg);
};
А приведённое для примера предложение по регуляризации войда напротив, нацелено на то, чтобы даже строку 6 тоже следать легальной.
Никита Колотов 28 января 2020, 0:20 Игорь Гусаров, сначала тут надо разобраться, действительно ли имеет место единообразие сценариев. Возьмем ваш пример и добавим проверку аргумента на соответствие заявленному типу:
#include <type_traits>
struct MyStruct{};
using T1 = int; // Built-in integer type
using T2 = MyStruct; // User-defined complete type
using T3 = void (*)(); // Pointer to function
using T4 = void (&)(); // Reference to function
using T5 = void (); // Function
void foo1(T1 arg){ static_assert(std::is_same_v<T1, decltype(arg)>); }// 1 ok
void foo2(T2 arg){ static_assert(std::is_same_v<T2, decltype(arg)>); }// 2 ok
void foo3(T3 arg){ static_assert(std::is_same_v<T3, decltype(arg)>); }// 3 ok
void foo4(T4 arg){ static_assert(std::is_same_v<T4, decltype(arg)>); }// 4 ok
void foo5(T5 arg){ static_assert(std::is_same_v<T5, decltype(arg)>); }// 5 err
варианты 1-4 работают, а 5 вариант внезапно вызовет ошибку. Или вот немного видоизменненый пример, домонстрирующий применение const квалификатора:
using T1 = int *; // Pointer to built-in integer type
using T2 = int; // Built-in integer type
using T3 = void (*) ();// Pointer to function
using T4 = void (); // Function
void foo1(T1 const arg){ arg = 0; }// 1 err
void foo2(T2 const arg){ arg = 0; }// 2 err
void foo3(T3 const arg){ arg = 0; }// 3 err
void foo4(T4 const arg){ arg = 0; }// 4 ok
теперь наоборот, работает только вариант, принимающий Function. Еще один пример, попробуем вместо разных функций сделать перегрузки:
using T1 = int; // Built-in integer type
using T2 = void ();// Function
void foo(T1 * arg){ }// 1 ok
void foo(T1 & arg){ }// 2 ok
void foo(T1 arg){ }// 3 ok
void foo(T2 * arg){ }// 4 ok
void foo(T2 & arg){ }// 5 ok
void foo(T2 arg){ }// 6 err
и опять с вариантом, принимающим просто Function не все в порядке...
Как видите, особого единообразия не наблюдается. Причин две:
Заметьте, мое предложение не уменьшает количество допустимых способов использования типа T5 - на этот тип уже наложены ограничения.
И вот мое предложение направлено на отмену этого второго правила (никак не затрагивая первое): раз уж функции не могут быть типом аргументов, то пусть не будет разрашено указывать их типом аргументов. Будет чуть более единообразно - типы аргументов всегда будут соответствовать объявленным.
Игорь Гусаров 28 января 2020, 19:42 Никита Колотов,
Про "функция не может быть типом аргумента функции". Мне кажется, в этом утверждении смешиваются понятия объекта и типа. Объект функции (её тело) действительно нельзя передать куда бы то ни было по значению, с этим я согласен. На объект ограничения есть. Но тип функции - сейчас можно передать, причём как раз благодаря специальным правилам. То есть на использование именно типа сейчас ограничений нет.
Про пример с "arg = 0;" Боюсь, что поведение, которое демонстрирует этот пример, вызвано тем, что к уже определённому функциональному типу в принципе нельзя добавить const-квалификацию. Она игнорируется. Деклараторы "T" и "const T" для функций - это в принципе одно и то же, независимо от того, где встречается такой декларатор. Т.е. это свойство никак не связано с использованием функции в качестве аргумента, и соответственно, предлагаемый запрет никак не повлияет на данное свойство.
Проиллюстрировать можно на специализации шаблона класса (так как тип в параметре шаблона всегда используется как он есть, сохраняя cv-квалификаторы и не деградируя до указателей):
using Test = void ();
//using Test = int&; // References are also like that.
template <typename T>
struct Probe;
// Full specialization for T = Test.
template <>
struct Probe<Test>
{
};
// Full specialization for T = const Test.
// Ooops... error: Redefinition of Probe<void()>
// Because const cannot be added
template <>
struct Probe<const Test>
{
};
Как видим, отбрасывание квалификатора const для функционального типа никак не связано с передачей агрументов в функцию. Можно заметить, что такое же поведение свойственно ссылочным типам. И это в общем-то логично, так как функции по смыслу гораздо ближе к семантике ссылки, а не значения.
Никита Колотов 29 января 2020, 23:14 Игорь Гусаров, 1. Не знаю, что вы подразумеваете "тип функции - сейчас можно передать". Вот в приведенном ранее примере с "void foo5(T5 arg)" тип T5 является функций, но не является типом аргмента. А правило разрешающее только видимость такой передачи является неконсистентным и только сбивает толку.
3 std::function не является равноценной заменой для простой передачи функций по указателю или по ссылке, хотя бы даже из-за оверхеда и невозможности использовать в constexpr.
Игорь Гусаров 30 января 2020, 15:40 Никита Колотов,
Вы привели несколько других интересных сценариев, в которых в теле такой функции можно наблюдать некрасивые эфекты. Хорошо. Но зачем из-за этих сценариев запрещать упомянутый выше вполне себе полезный сценарий? Ведь не во всех функциях нужно проверять is_same и пытаться присвоить что-то аргументу...
Про объяснение отбрасывания const. Извините, если у меня не получилось пересказать смысл [dcl.func].p7. Я старался передать факт практически дословно: "к уже определённому функциональному типу в принципе нельзя добавить const-квалификацию. Она игнорируется."
Согласен, не равноценна. Но почему же Вы видите те недостатки, но не видите недостатков у запрета?
Перенос предложения: голоса +5, -2 Автор идеи: Никита Колотов
Несоответствие объявленного типа таких параметров фактическому (например в int foo(int action()) параметр action - это указатель) затрудняет работу с ними провоцирует возникновение дефектов в программах.
С давних времен в С и С++ имеется специальное правило, по которому параметры функций с типом функция T на деле получают тип указатель на T: [dcl.fct] 11.3.5 Functions 5 ... After determining the type of each parameter, any parameter of type “array of T” or of function type T is adjusted to be “pointer to T”. C параметрами-функциями сложностей меньше, чем с параметрами-массивами, но они тоже зачастую приводят к различным проблемам:
void test(action_t const action) // const is not applied to pointer { action = 0; // wat }
Соответственно я предлагаю это правило заменить на прямой запрет таких действий: If function has parameter of type “function T” the program is ill-formed.