Почему экземпляры std::function имеют конструктор по умолчанию?

Наверное, это философский вопрос, но я столкнулся со следующей проблемой:

Если вы определите std::function и не инициализируете ее правильно, ваше приложение рухнет, например так:

typedef std::function<void(void)> MyFunctionType;
MyFunctionType myFunction;
myFunction();

Если функция передается в качестве аргумента, например:

void DoSomething (MyFunctionType myFunction)
   {
   myFunction();
   }

Потом, конечно, тоже вылетает. Это означает, что я вынужден добавить код проверки следующим образом:

void DoSomething (MyFunctionType myFunction)
   {
   if (!myFunction) return;
   myFunction();
   }

Требование этих проверок возвращает меня к старым дням C, когда вы также должны были явно проверять все аргументы указателя:

void DoSomething (Car *car, Person *person)
   {
   if (!car) return;      // In real applications, this would be an assert of course
   if (!person) return;   // In real applications, this would be an assert of course
   ...
   }

К счастью, мы можем использовать ссылки на C++, что не позволяет мне писать эти проверки (при условии, что вызывающая сторона не передала в функцию содержимое nullptr:

void DoSomething (Car &car, Person &person)
   {
   // I can assume that car and person are valid
   }

Итак, почему экземпляры std::function имеют конструктор по умолчанию? Без конструктора по умолчанию вам не пришлось бы добавлять проверки, как и для других обычных аргументов функции. И в тех «редких» случаях, когда вы хотите передать «необязательную» функцию std::, вы все равно можете передать на нее указатель (или использовать boost:: optional).


person Patrick    schedule 23.09.2011    source источник
comment
Он не падает; он выдает исключение.   -  person Kaz Dragon    schedule 23.09.2011
comment
Прочтите о functors: sgi.com/tech/stl/functors.html   -  person Marcin Gil    schedule 23.09.2011
comment
Я вынужден добавить проверочный код — жаль, что ваши абоненты не могут вместо этого исправить свой код. Кажется странным прервать, чтобы избавить их от необходимости обрабатывать исключение. Тем не менее, могло быть и хуже, если бы вы взяли указатель на функцию и не удосужились его инициализировать, тогда он имел бы неопределенное значение и поведение было бы неопределенным. Вероятно, они гораздо хуже запутались, вызывая strlen, чем вызывая вашу функцию.   -  person Steve Jessop    schedule 23.09.2011
comment
Что вы будете делать, если кто-то передаст нулевой указатель в функцию std::function? Тогда большинство людей, скорее всего, инициализируют их всех фиктивной функцией, которая все равно выдает генерацию, так что ничего не выиграло.   -  person PlasmaHH    schedule 23.09.2011
comment
Если вы хотите создать функцию по умолчанию no-op, а не функцию, выбрасывающую исключение, это довольно просто — до тех пор, пока вы возвращаете void. Обработка всех типов (включая ссылки) на удивление сложна: stackoverflow.com/q/31274869/1858225   -  person Kyle Strand    schedule 07.07.2015


Ответы (7)


Верно, но это верно и для других типов. Например. если я хочу, чтобы в моем классе был необязательный объект Person, я делаю свой элемент данных указателем на человека. Почему бы не сделать то же самое для std::functions? Что такого особенного в std::function, что он может иметь «недопустимое» состояние?

У него нет «недопустимого» состояния. Это не более недействительно, чем это:

std::vector<int> aVector;
aVector[0] = 5;

У вас есть пустой function, точно так же, как aVector — это пустой vector. Объект находится в очень четко определенном состоянии: состоянии отсутствия данных.

Теперь давайте рассмотрим ваше предложение «указатель на функцию»:

void CallbackRegistrar(..., std::function<void()> *pFunc);

Как вы должны называть это? Что ж, вот что вы не можете сделать:

void CallbackFunc();
CallbackRegistrar(..., CallbackFunc);

Это не разрешено, потому что CallbackFunc — это функция, а тип параметра — std::function<void()>*. Эти два не конвертируются, поэтому компилятор будет жаловаться. Итак, чтобы сделать звонок, вы должны сделать это:

void CallbackFunc();
CallbackRegistrar(..., new std::function<void()>(CallbackFunc));

Вы только что представили new на картинке. Вы выделили ресурс; кто будет нести ответственность за это? CallbackRegistrar? Очевидно, вы можете захотеть использовать какой-нибудь интеллектуальный указатель, чтобы еще больше загромождать интерфейс:

void CallbackRegistrar(..., std::shared_ptr<std::function<void()>> pFunc);

Это много раздражения и бесполезности API, просто чтобы передать функцию. Самый простой способ избежать этого — оставить std::function пустым. Так же, как мы позволяем std::vector быть пустым. Так же, как мы позволяем std::string быть пустым. Так же, как мы позволяем std::shared_ptr быть пустым. И так далее.

Проще говоря: std::function содержит функцию. Это держатель для вызываемого типа. Следовательно, существует вероятность того, что он не содержит вызываемого типа.

person Nicol Bolas    schedule 23.09.2011
comment
Очевидно, вы хотели бы, чтобы boost::optional<std::function< был там. Проблема в том, что вы можете создать необязательный тип из необязательного типа, но не наоборот. Поэтому типы по умолчанию должны быть необязательными. - person MSalters; 23.09.2011
comment
@MSalters: Да, это сработает. Но, к сожалению, optional не является частью стандартной библиотеки C++. А для тех бедных, несчастных душ, которым не разрешено использовать Boost, это просто не вариант. - person Nicol Bolas; 23.09.2011
comment
Голосуйте, хорошая формулировка: необязательно... это просто не вариант, ха-ха! - person Germán Diago; 29.10.2014
comment
Я думаю, что ваш векторный пример отличается. Ничего полезного вы не можете сделать с пустой std::function, кроме присвоения значения. Он не содержит значения. Вектор, когда он построен, уже является вектором. Вы можете использовать большую часть его интерфейса (например, push_back()). В случае функции я бы сказал, что конструктор по умолчанию на самом деле не имеет смысла. - person Germán Diago; 29.10.2014
comment
std::vector<int> содержит данные; он содержит данные о том, что количество элементов в векторе равно 0, и он содержит такое же количество элементов (0). Поэтому он находится в действительном состоянии. Вы можете индексировать его, что является недопустимой операцией, которая может привести к сбою вашей программы. Сам объект все еще находится в допустимом состоянии. std::function находится в недопустимом состоянии, если оно не инициализировано -- вы ничего не можете с ним сделать, поскольку единственная операция, которую вы можете с ним сделать, это переназначить его какой-либо функции, приведя его в допустимое состояние или вызвать его, что будем надеяться, что ваша программа выйдет из строя. - person Clearer; 15.01.2020

На самом деле ваше приложение не должно падать.

§ 20.8.11.1 Класс bad_function_call [func.wrap.badcall]

1/ Исключение типа bad_function_call создается function::operator() (20.8.11.2.4), когда у объекта-оболочки функции нет цели.

Поведение точно определено.

person Matthieu M.    schedule 23.09.2011
comment
Это может быть указано, но спецификация также говорит, что программа завершает работу (т.е. аварийно завершает работу) в случае возникновения исключения, оставляющего main. Вот что бывает, если не поймать. - person Nicol Bolas; 23.09.2011
comment
@NicolBolas: Вы правы, но на самом деле я не считаю это сбоем, поскольку многие операции потенциально могут вызвать исключение: простой факт назначения чего-либо std::function может привести к выделению памяти. Следовательно, отсутствие обработки исключений совершенно не связано с тем фактом, что std::function может не инициализироваться обратным вызовом. - person Matthieu M.; 23.09.2011

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

person Nicola Musatti    schedule 23.09.2011
comment
Верно, но это верно и для других типов. Например. если я хочу, чтобы в моем классе был необязательный объект Person, я делаю свой элемент данных указателем на человека. Почему бы не сделать то же самое для std::functions? Что такого особенного в std::function, что он может иметь «недопустимое» состояние? - person Patrick; 23.09.2011
comment
На мой взгляд, вы должны рассматривать std::function как своего рода интеллектуальный указатель для вызываемых объектов. Вы же не ожидаете использовать указатель на shared_ptr, не так ли? - person Nicola Musatti; 23.09.2011

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

Идентифицируемое недопустимое состояние на самом деле не нужно, поскольку, как вы упомянули, boost::optional прекрасно справляется с этой задачей. Так что я бы сказал, что std::function здесь просто ради истории.

person thiton    schedule 23.09.2011

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

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

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

person Emilio Garavaglia    schedule 23.09.2011
comment
Я не думаю, что есть способ исправить позже пустой std::function. Если он пуст при создании, он всегда пуст. (Конечно, считаете ли вы это недопустимым состоянием или нет, это другой вопрос.) - person max; 02.02.2018
comment
@max выглядит как та же концепция - для меня другая проблема формулировки. - person Emilio Garavaglia; 02.02.2018
comment
Наверное, тогда я не понял вашего аргумента. Вы говорите, что можете использовать пустой std::function, когда вы не знаете, какое значение вы хотите для него, пока позже (то есть вы не можете инициализировать его в ctor). Я согласен, что это был бы хороший вариант использования. Чего я не вижу, так это того, что вы сделаете позже, когда, наконец, сделаете то, что вам нужно. Не похоже, что вы можете заменить пустой std::function на что-то другое? - person max; 25.02.2018

Точно так же, как вы можете добавить нулевое состояние к типу функтора, у которого его нет, вы можете обернуть функтор классом, который не допускает нулевое состояние. Первый требует добавления состояния, второй не требует нового состояния (только ограничение). Таким образом, хотя я не знаю, в чем смысл дизайна std::function, он поддерживает наиболее экономичное и среднее использование, независимо от того, что вы хотите.

Ура и чт.,

person Cheers and hth. - Alf    schedule 23.09.2011

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

template <typename Callback, typename... Ts>
void SendNotification(const Callback & callback, Ts&&... vs)
{
    if (callback)
    {
        callback(std::forward<Ts>(vs)...);
    }
}

И использовать его следующим образом:

std::function<void(int, double>> myHandler;
...
SendNotification(myHandler, 42, 3.15);
person Aleksey Malov    schedule 26.09.2015