Работая над упражнением для моего класса C++, где мы работали над обработкой исключений, я столкнулся с загадочной проблемой. Изначально у меня был следующий обработчик исключений (пожалуйста, не критикуйте использование int
в качестве исключений, это была задача, которую нам дали):
#include <iostream>
//...
try {
// Code that throws an int
}
catch(const int errno) {
std::cout << errno << std::endl;
}
Неподготовленному глазу это может показаться обычным объявлением целочисленной переменной исключения. Однако небезопасный код продолжал обходить блок catch, несмотря на то, что сам выбрасывал целое число. После некоторого расследования выяснилось, что макрос в errno.h
объявленном #define errno (*__errno_location ())
вызвал расширение макроса в следующий код
try {
// Code that throws an int
}
catch(const int (*__errno_location())) {
std::cout << (*__errno_location()) << std::endl;
}
Я не был уверен, что именно объявляется в выражении catch. Я попробовал использовать C Gibberish для английского, но неудивительно, что это не сильно помогло, сообщив мне, что это объявление функции, возвращающей int*
.
Немного поигравшись с этим, я придумал следующий код:
const int *blah() {
return new int(10);
}
int main() {
try {
throw &blah;
}
catch(const int *foo()) {
std::cout << *foo () << std::endl;
}
}
Который выводит на консоль число 10. Изменение значений в конструкторе целых чисел также приводит к выводу этого целого числа на консоль. Итак, моя текущая гипотеза заключается в том, что const int *foo()
объявляет, что указатель функции, возвращающий указатель int, должен быть пойман в этом блоке catch, а в операторе печати мы просто вызываем эту функцию и разыменовываем указатель. Однако мне хотелось бы, чтобы мою гипотезу проверили или исправили более опытные программисты на C++.
Ваше исследование верно. В C++ можно использовать любой тип, включая функции и указатели на функции. Вы можете сделать его более читабельным, используя using FuncType = const int *();
и catch (FuncType foo)
.
Вызов оператора new
в функции blah
приводит к утечке памяти, поскольку соответствующий delete
нигде не найден.
errno
определена реализация. См. cppreference:
errno — это макрос препроцессора, используемый для индикации ошибок. Он расширяется до статического (до C++11) локального потока (начиная с C++11) изменяемого lvalue типа
int
.
Из того, что вы уже узнали, мы можем сделать вывод, что __errno_location()
— это функция (или какой-либо вызываемый объект), возвращающая указатель на int
, который кодирует последнюю ошибку в используемой вами реализации.
Как упоминалось в комментарии, идентификатор errno
зарезервирован POSIX.
Теперь то, что происходит в вашем коде, примерно следующее:
int* foo() {
static int x = 42;
return &x;
}
int main() {
try {
} catch(const int (*foo()) ){
}
}
Который тоже компилируется (см. здесь: https://godbolt.org/z/3v8nsPr74).
Причина в том, что const int (*foo())
на самом деле объявляет параметр с именем foo
типа const int(*)()
, то есть указатель на функцию без параметра и возвращает const int
.
И, как вы уже выяснили, название — просто совпадение. Ваш параметр называется __errno_location
, и внутри блока catch
он затеняет фактическую функцию с тем же именем.
Урок, который нужно усвоить, заключается в том, насколько злы макросы. Они загрязняют глобальное пространство имен. Вот почему вы были удивлены, используя errno
в качестве имени параметра. А во-вторых, макросы — это зло, потому что невинное на вид имя может превратить const int*
во что-то совершенно иное. К сожалению, здесь у вас нет выбора не использовать макрос. Вам только нужно быть осторожным, чтобы избежать этого имени.
@Yksisarvinen он компилируется, потому что *__errno_location()
— это изменяемое lvalue типа int… о, ок, теперь я понимаю, что вы имеете в виду. Как сильно я не люблю, когда меня принимают, когда я не прав :/
Удалил мой комментарий, так как ОП принял ваш ответ, поэтому я думаю, что я неправильно его интерпретировал.
@Yksisarvinen нет, я думаю, ты был прав. Я не понимаю, почему код компилируется, и я действительно не отвечал на этот вопрос, когда писал это.
Принял ответ до того, как просмотрел комментарии... Итак, я полагаю, загадка еще не раскрыта? Я не решаюсь сказать, что мы объявили переменную-указатель функции, потому что, придя к форме C, я ожидал, что объявление будет return_type (*name)(args...)
, что близко, но не совсем к тому, до чего расширяется фактический код.
@Кех, я пока не понимаю, почему это компилируется godbolt.org/z/TaxWsv51c ? Возможно, просто, но я этого не вижу, и мне нужно уйти, поэтому предлагаю вам на данный момент отказаться от этого.
@Keheck IMO, вы уже написали это в своем вопросе - он интерпретируется как тип функции, не принимает аргументов, возвращает const int*
, и такой объект функции, когда он пойман, называется __errno_location
.
Да, это теория, основанная на фактах, но мне интересно, почему ее можно так заявлять. Как я уже сказал, я ожидал, что синтаксис будет const int* (*foo)()
или что-то подобное, поскольку именно так вы обычно объявляете указатели на функции (в данном случае функция, возвращающая int*
без аргументов). Возможно, мне не хватает небольшой детали в C++.
@Keheck Деталь может заключаться в том, что существуют типы функций и типы указателей функций. Это очень запутанная особенность языка, поскольку, когда вы объявляете тип функции как параметр, вы действительно получаете указатель на функцию, а когда вы передаете функцию другой функции, и foo
, и &foo
преобразуются в указатель на функцию. И вызов указателя функции не требует предварительного разыменования указателя...
Возможно, моя формулировка была немного неясной. Я имел в виду, что мы вызываем функцию foo
, а затем разыменовываем возвращаемый ею указатель int. Интересно, является ли разница между типами указателей функций и типами функций особенностью C++ или типы функций существуют и в C? Я знаю, что у него есть типы указателей на функции
@Keheck Я снова отредактировал, и теперь, по моему мнению, ответ полон. Однако все части уже присутствуют в вашем вопросе. Я лишь повторяю то, что вы уже сказали.
Похоже, что он анализируется как функция, не принимающая никаких параметров, возвращающих const int*
: godbolt.org/z/hhx71jxrc (Обратите внимание, что catch
настраивается так же, как и параметры функции, поэтому foo
объявляется с типом функции и адаптируется к указателю - тип функции)
В коде не должно было использоваться имя переменной
errno
. Это зарезервированное имя, и оно вызывает побочные эффекты. Заменитеerrno
на другое имя переменной, и все будет в порядке. Также обратите внимание, что рекомендуется перехватывать типы исключений с помощьюconst&
(для int это не так важно). Так что ставьтеcatch(const int& value) std::cout << value << "\n"
и все будет в порядке.