Запрет на использование функций с передачей "по значению" в качестве типов параметров при объявлении функций.

Никита Колотов
Никита Колотов

С давних времен в С и С++ имеется специальное правило, по которому параметры функций с типом функция 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 параметрами-функциями сложностей меньше, чем с параметрами-массивами, но они тоже зачастую приводят к различным проблемам:
1. игнорирование опциональности такого параметра:

void foo(int bar(int))
{
    cout << bar(2); // fail
}
...
foo(nullptr);

2. неочевидная невозможность задать const квалификатор для такого параметра чтобы соблюсти const-correctness:

using action_t = int (int);

void test(action_t const action) // const is not applied to pointer
{
    action = 0; // wat
}

3. могут иметь место расхождения между объявлениями функции  и / или ее определением, создавая неразбериху:

using action_t = int (int);

void test(action_t action);
void test(action_t * p_action); // same as above

4. дополнительные сложности в случае с most vexing parse:

int x(int()); // парсится не как переменная x, а как объявление функции
...
x = 4; // компилятор сообщит об ошибке тут, а не в месте некорректного объявления x

Соответственно я предлагаю это правило заменить на прямой запрет таких действий: If function has parameter of type “function T” the program is ill-formed.

2
рейтинг
7 комментариев
Игорь Гусаров

1. Обратная совместимость.
Любое предложение, которое ломает существующий код (даже с самыми благими намерениями!), с большой вероятностью будет отклонено. Для выдвижения ломающих предложений нужно, чтобы был показан _колоссальный_ выигрыш от них. Боюсь, что в данном случае выигрыш звучит не очень убедительно: "защититься от возможных ошибок кодирования, от которых и так можно защититься имеющимися в языке средствами".

2. Унификация.
С точки зрения generic programming лучше, чтобы все типы вели себя по возможности единообразно. Например, есть предложение P0146R1 Regular Void. Оно нацелено на унификацию войда с остальными типами, чтобы в шаблонном коде можно было написать "auto x = foo();" или "promise.set_value(foo());" даже в случае, когда foo возвращает void. Это предложение рассматривается комитетом и уже дошло до стадии пробной реализации.

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

Игорь Гусаров
Никита Колотов

Игорь Гусаров, Мне кажется, что вы как-то не так поняли смысл этого предложения. На единообразие типов оно влияет только положительно, так как направлено на отмену специального правила для интерпретации типов аргументов фунции и переход к единообразным правилам на этот счет для всех типов. Заметьте, что оно затрагивает написание сигнатуры функции, т.е. для выражений "auto x = foo();" или "promise.set_value(foo());" оно вообще не применяется.

Никита Колотов
Игорь Гусаров

Никита Колотов, возможно, я действительно как-то не так понял. Вы говорите о единообразном описании типов в стандарте?

А я говорю уже о следующем шаге: как это предложение отразится на единообразии сценариев использования языка:

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 тоже следать легальной.

Игорь Гусаров
Обновлено 
Никита Колотов

Игорь Гусаров, сначала тут надо разобраться, действительно ли имеет место единообразие сценариев. Возьмем ваш пример и добавим проверку аргумента на соответствие заявленному типу:

#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 не все в порядке...

Как видите, особого единообразия не наблюдается. Причин две:

1. функция не может быть типом аргумента функции

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

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

И вот мое предложение направлено на отмену этого второго правила (никак не затрагивая первое): раз уж функции не могут быть типом аргументов, то пусть не будет разрашено указывать их типом аргументов. Будет чуть более единообразно - типы аргументов всегда будут соответствовать объявленным.

 

Никита Колотов
Игорь Гусаров

Никита Колотов,

1. Про "функция не может быть типом аргумента функции".
Мне кажется, в этом утверждении смешиваются понятия объекта и типа. Объект функции (её тело) действительно нельзя передать куда бы то ни было по значению, с этим я согласен. На объект ограничения есть. Но тип функции - сейчас можно передать, причём как раз благодаря специальным правилам. То есть на использование именно типа сейчас ограничений нет.

2. Про пример с "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 для функционального типа никак не связано с передачей агрументов в функцию. Можно заметить, что такое же поведение свойственно ссылочным типам. И это в общем-то логично, так как функции по смыслу гораздо ближе к семантике ссылки, а не значения.

3. Про примеры с is_same_v и с перегрузкой.
Вы правы, это неприятные особенности. К ним можно ещё добавить невозможность сделать копию аргумента в теле функции: T5 temp = arg;. Подобные проблемы с перегрузкой возникают и со ссылочными типами (перегрузка по T и T& в случае, когда T - ссылка). Сейчас подобных проблем можно избежать, если пользоваться std::function и std::array. Но я с удовольствием поддержу предложение, конструктивно улучшающее общую ситуацию, в частности, не ломающее существующий код и не запрещающее видимость передачи по значению, т.к. такая передача востребована в обобщённом коде.

Игорь Гусаров
Никита Колотов

Игорь Гусаров, 1. Не знаю, что вы подразумеваете "тип функции - сейчас можно передать". Вот в приведенном ранее примере с "void foo5(T5 arg)" тип T5 является функций, но не является типом аргмента. А правило разрешающее только видимость такой передачи является неконсистентным и только сбивает толку.

2. Ваше объяснение отбрасывания const квалификатора является не совсем верным. В данной ситуации он в принципе не учитывается, даже если бы и имыл смыл приминительно к функции. Для массивов он имеет смысл, но также полностью игнорируется при замене типа. Но собственно механизм игнорирования тут не так важен, важно то, что аргумент всегда остается изменяемым.

3 std::function не является равноценной заменой для простой передачи функций по указателю или по ссылке, хотя бы даже из-за оверхеда и невозможности использовать в constexpr.

Никита Колотов
Игорь Гусаров

Никита Колотов,
1. Про "Не знаю, что вы подразумеваете "тип функции - сейчас можно передать"." - я имею в виду именно то, что написал в этом сообщении: что void foo5(T5 arg) является валидным объявлением функции. Т.е. что данный тип можно использовать как тип формального параметра. Это - уже сценарий. Он уже позволяет определить функцию и что-то в ней сделать. И единообразие этого конкретного сценария уже ценно.

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

2. Про объяснение отбрасывания const.
Извините, если у меня не получилось пересказать смысл [dcl.func].p7. Я старался передать факт практически дословно: "к уже определённому функциональному типу в принципе нельзя добавить const-квалификацию. Она игнорируется."

3. Согласен, не равноценна. Но почему же Вы видите те недостатки, но не видите недостатков у запрета?

Игорь Гусаров
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения