Время на прочтение
13 мин
Количество просмотров 74K
Введение
Ошибки, увы, неизбежны, поэтому их обработка занимает очень важное место в программировании. И если алгоритмические ошибки можно выявить и исправить во время написания и тестирования программы, то ошибок времени выполнения избежать нельзя в принципе. Сегодня мы рассмотрим функции стандартной библиотеки (C Standard Library) и POSIX, используемые в обработке ошибок.
Переменная errno и коды ошибок
<errno.h>
errno – переменная, хранящая целочисленный код последней ошибки. В каждом потоке существует своя локальная версия errno, чем и обусловливается её безопасность в многопоточной среде. Обычно errno реализуется в виде макроса, разворачивающегося в вызов функции, возвращающей указатель на целочисленный буфер. При запуске программы значение errno равно нулю.
Все коды ошибок имеют положительные значения, и могут использоваться в директивах препроцессора #if. В целях удобства и переносимости заголовочный файл <errno.h>
определяет макросы, соответствующие кодам ошибок.
Стандарт ISO C определяет следующие коды:
- EDOM – (Error domain) ошибка области определения.
- EILSEQ – (Error invalid sequence) ошибочная последовательность байтов.
- ERANGE – (Error range) результат слишком велик.
Прочие коды ошибок (несколько десятков) и их описания определены в стандарте POSIX. Кроме того, в спецификациях стандартных функций обычно указываются используемые ими коды ошибок и их описания.
Нехитрый скрипт печатает в консоль коды ошибок, их символические имена и описания:
#!/usr/bin/perl
use strict;
use warnings;
use Errno;
foreach my $err (sort keys (%!)) {
$! = eval "Errno::$err";
printf "%20s %4d %sn", $err, $! + 0, $!
}
Если вызов функции завершился ошибкой, то она устанавливает переменную errno в ненулевое значение. Если же вызов прошёл успешно, функция обычно не проверяет и не меняет переменную errno. Поэтому перед вызовом функции её нужно установить в 0
.
Пример:
/* convert from UTF16 to UTF8 */
errno = 0;
n_ret = iconv(icd, (char **) &p_src, &n_src, &p_dst, &n_dst);
if (n_ret == (size_t) -1) {
VJ_PERROR();
if (errno == E2BIG)
fprintf(stderr, " Error : input conversion stopped due to lack of space in the output buffern");
else if (errno == EILSEQ)
fprintf(stderr, " Error : input conversion stopped due to an input byte that does not belong to the input codesetn");
else if (errno == EINVAL)
fprintf(stderr, " Error : input conversion stopped due to an incomplete character or shift sequence at the end of the input buffern");
/* clean the memory */
free(p_out_buf);
errno = 0;
n_ret = iconv_close(icd);
if (n_ret == (size_t) -1)
VJ_PERROR();
return (size_t) -1;
}
Как видите, описания ошибок в спецификации функции iconv()
более информативны, чем в <errno.h>
.
Функции работы с errno
Получив код ошибки, хочется сразу получить по нему её описание. К счастью, ISO C предлагает целый набор полезных функций.
<stdio.h>
void perror(const char *s);
Печатает в stderr содержимое строки s
, за которой следует двоеточие, пробел и сообщение об ошибке. После чего печатает символ новой строки 'n'
.
Пример:
/*
// main.c
// perror example
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, const char * argv[])
{
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(file_name, "rb");
if (file) {
// Do something useful.
fclose(file);
}
else {
perror("fopen() ");
}
return EXIT_SUCCESS;
}
<string.h>
char* strerror(int errnum);
Возвращает строку, содержащую описание ошибки errnum
. Язык сообщения зависит от локали (немецкий, иврит и даже японский), но обычно поддерживается лишь английский.
/*
// main.c
// strerror example
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, const char * argv[])
{
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(file_name, "rb");
// Save error number.
errno_t error_num = errno;
if (file) {
// Do something useful.
fclose(file);
}
else {
char *errorbuf = strerror(error_num);
fprintf(stderr, "Error message : %sn", errorbuf);
}
return EXIT_SUCCESS;
}
strerror()
не безопасная функция. Во-первых, возвращаемая ею строка не является константной. При этом она может храниться в статической или в динамической памяти в зависимости от реализации. В первом случае её изменение приведёт к ошибке времени выполнения. Во-вторых, если вы решите сохранить указатель на строку, и после вызовите функцию с новым кодом, все прежние указатели будут указывать уже на новую строку, ибо она использует один буфер для всех строк. В-третьих, её поведение в многопоточной среде не определено в стандарте. Впрочем, в QNX она объявлена как thread safe.
Поэтому в новом стандарте ISO C11 были предложены две очень полезные функции.
size_t strerrorlen_s(errno_t errnum);
Возвращает длину строки с описанием ошибки errnum
.
errno_t strerror_s(char *buf, rsize_t buflen, errno_t errnum);
Копирует строку с описание ошибки errnum
в буфер buf
длиной buflen
.
Пример:
/*
// main.c
// strerror_s example
//
// Created by Ariel Feinerman on 23/02/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, const char * argv[])
{
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(file_name, "rb");
// Save error number.
errno_t error_num = errno;
if (file) {
// Do something useful.
fclose(file);
}
else {
#ifdef __STDC_LIB_EXT1__
size_t error_len = strerrorlen_s(errno) + 1;
char error_buf[error_len];
strerror_s(error_buf, error_len, errno);
fprintf(stderr, "Error message : %sn", error_buf);
#endif
}
return EXIT_SUCCESS;
}
Функции входят в Annex K (Bounds-checking interfaces), вызвавший много споров. Он не обязателен к выполнению и целиком не реализован ни в одной из свободных библиотек. Open Watcom C/C++ (Windows), Slibc (GNU libc) и Safe C Library (POSIX), в последней, к сожалению, именно эти две функции не реализованы. Тем не менее, их можно найти в коммерческих средах разработки и системах реального времени, Embarcadero RAD Studio, INtime RTOS, QNX.
Стандарт POSIX.1-2008 определяет следующие функции:
char *strerror_l(int errnum, locale_t locale);
Возвращает строку, содержащую локализованное описание ошибки errnum
, используя locale
. Безопасна в многопоточной среде. Не реализована в Mac OS X, FreeBSD, NetBSD, OpenBSD, Solaris и прочих коммерческих UNIX. Реализована в Linux, MINIX 3 и Illumos (OpenSolaris).
Пример:
/*
// main.c
// strerror_l example – works on Linux, MINIX 3, Illumos
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <locale.h>
int main(int argc, const char * argv[])
{
locale_t locale = newlocale(LC_ALL_MASK, "fr_FR.UTF-8", (locale_t) 0);
if (!locale) {
fprintf(stderr, "Error: cannot create locale.");
exit(EXIT_FAILURE);
}
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(tmpnam(file_name, "rb");
// Save error number.
errno_t error_num = errno;
if (file) {
// Do something useful.
fclose(file);
}
else {
char *error_buf = strerror_l(errno, locale);
fprintf(stderr, "Error message : %sn", error_buf);
}
freelocale(locale);
return EXIT_SUCCESS;
}
Вывод:
Error message : Aucun fichier ou dossier de ce type
int strerror_r(int errnum, char *buf, size_t buflen);
Копирует строку с описание ошибки errnum
в буфер buf
длиной buflen
. Если buflen
меньше длины строки, лишнее обрезается. Безопасна в многоготочной среде. Реализована во всех UNIX.
Пример:
/*
// main.c
// strerror_r POSIX example
//
// Created by Ariel Feinerman on 25/02/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MSG_LEN 1024
int main(int argc, const char * argv[])
{
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(file_name, "rb");
// Save error number.
errno_t error_num = errno;
if (file) {
// Do something useful.
fclose(file);
}
else {
char error_buf[MSG_LEN];
errno_t error = strerror_r (error_num, error_buf, MSG_LEN);
switch (error) {
case EINVAL:
fprintf (stderr, "strerror_r() failed: invalid error code, %dn", error);
break;
case ERANGE:
fprintf (stderr, "strerror_r() failed: buffer too small: %dn", MSG_LEN);
case 0:
fprintf(stderr, "Error message : %sn", error_buf);
break;
default:
fprintf (stderr, "strerror_r() failed: unknown error, %dn", error);
break;
}
}
return EXIT_SUCCESS;
}
Увы, никакого аналога strerrorlen_s()
в POSIX не определили, поэтому длину строки можно выяснить лишь экспериментальным путём. Обычно 300 символов хватает за глаза. GNU C Library в реализации strerror()
использует буфер длиной в 1024 символа. Но мало ли, а вдруг?
Пример:
/*
// main.c
// strerror_r safe POSIX example
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MSG_LEN 1024
#define MUL_FACTOR 2
int main(int argc, const char * argv[])
{
// Generate unique filename.
char *file_name = tmpnam((char[L_tmpnam]){0});
errno = 0;
FILE *file = fopen(file_name, "rb");
// Save error number.
errno_t error_num = errno;
if (file) {
// Do something useful.
fclose(file);
}
else {
errno_t error = 0;
size_t error_len = MSG_LEN;
do {
char error_buf[error_len];
error = strerror_r (error_num, error_buf, error_len);
switch (error) {
case 0:
fprintf(stderr, "File : %snLine : %dnCurrent function : %s()nFailed function : %s()nError message : %sn", __FILE__, __LINE__, __func__, "fopen", error_buf);
break;
case ERANGE:
error_len *= MUL_FACTOR;
break;
case EINVAL:
fprintf (stderr, "strerror_r() failed: invalid error code, %dn", error_num);
break;
default:
fprintf (stderr, "strerror_r() failed: unknown error, %dn", error);
break;
}
} while (error == ERANGE);
}
return EXIT_SUCCESS;
}
Вывод:
File : /Users/ariel/main.c
Line : 47
Current function : main()
Failed function : fopen()
Error message : No such file or directory
Макрос assert()
<assert.h>
void assert(expression)
Макрос, проверяющий условие expression
(его результат должен быть числом) во время выполнения. Если условие не выполняется (expression
равно нулю), он печатает в stderr значения __FILE__
, __LINE__
, __func__
и expression
в виде строки, после чего вызывает функцию abort()
.
/*
// main.c
// assert example
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
int main(int argc, const char * argv[]) {
double x = -1.0;
assert(x >= 0.0);
printf("sqrt(x) = %fn", sqrt(x));
return EXIT_SUCCESS;
}
Вывод:
Assertion failed: (x >= 0.0), function main, file /Users/ariel/main.c, line 17.
Если макрос NDEBUG
определён перед включением <assert.h>
, то assert()
разворачивается в ((void) 0)
и не делает ничего. Используется в отладочных целях.
Пример:
/*
// main.c
// assert_example
//
// Created by Ariel Feinerman on 23/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#NDEBUG
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
int main(int argc, const char * argv[]) {
double x = -1.0;
assert(x >= 0.0);
printf("sqrt(x) = %fn", sqrt(x));
return EXIT_SUCCESS;
}
Вывод:
sqrt(x) = nan
Функции atexit(), exit() и abort()
<stdlib.h>
int atexit(void (*func)(void));
Регистрирует функции, вызываемые при нормальном завершении работы программы в порядке, обратном их регистрации. Можно зарегистрировать до 32 функций.
_Noreturn void exit(int exit_code);
Вызывает нормальное завершение программы, возвращает в среду число exit_code
. ISO C стандарт определяет всего три возможных значения: 0
, EXIT_SUCCESS
и EXIT_FAILURE
. При этом вызываются функции, зарегистрированные через atexit()
, сбрасываются и закрываются потоки ввода — вывода, уничтожаются временные файлы, после чего управление передаётся в среду. Функция exit()
вызывается в main()
при выполнении return или достижении конца программы.
Главное преимущество exit()
в том, что она позволяет завершить программу не только из main()
, но и из любой вложенной функции. К примеру, если в глубоко вложенной функции выполнилось (или не выполнилось) некоторое условие, после чего дальнейшее выполнение программы теряет всякий смысл. Подобный приём (early exit) широко используется при написании демонов, системных утилит и парсеров. В интерактивных программах с бесконечным главным циклом exit()
можно использовать для выхода из программы при выборе нужного пункта меню.
Пример:
/*
// main.c
// exit example
//
// Created by Ariel Feinerman on 17/03/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
void third_2(void)
{
printf("third #2n"); // Does not print.
}
void third_1(void)
{
printf("third #1n"); // Does not print.
}
void second(double num)
{
printf("second : before exit()n"); // Prints.
if ((num < 1.0f) && (num > -1.0f)) {
printf("asin(%.1f) = %.3fn", num, asin(num));
exit(EXIT_SUCCESS);
}
else {
fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]n", num);
exit(EXIT_FAILURE);
}
printf("second : after exit()n"); // Does not print.
}
void first(double num)
{
printf("first : before second()n")
second(num);
printf("first : after second()n"); // Does not print.
}
int main(int argc, const char * argv[])
{
atexit(third_1); // Register first handler.
atexit(third_2); // Register second handler.
first(-3.0f);
return EXIT_SUCCESS;
}
Вывод:
first : before second()
second : before exit()
Error: -3.0 is beyond the range [-1.0; 1.0]
third #2
third #1
_Noreturn void abort(void);
Вызывает аварийное завершение программы, если сигнал не был перехвачен обработчиком сигналов. Временные файлы не уничтожаются, закрытие потоков определяется реализацией. Самое главное отличие вызовов abort() и exit(EXIT_FAILURE)
в том, что первый посылает программе сигнал SIGABRT
, его можно перехватить и произвести нужные действия перед завершением программы. Записывается дамп памяти программы (core dump file), если они разрешены. При запуске в отладчике он перехватывает сигнал SIGABRT
и останавливает выполнение программы, что очень удобно в отладке.
Пример:
/*
// main.c
// abort example
//
// Created by Ariel Feinerman on 17/02/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
void third_2(void)
{
printf("third #2n"); // Does not print.
}
void third_1(void)
{
printf("third #1n"); // Does not print.
}
void second(double num)
{
printf("second : before exit()n"); // Prints.
if ((num < 1.0f) && (num > -1.0f)) {
printf("asin(%.1f) = %.3fn", num, asin(num));
exit(EXIT_SUCCESS);
}
else {
fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]n", num);
abort();
}
printf("second : after exit()n"); // Does not print.
}
void first(double num)
{
printf("first : before second()n");
second(num);
printf("first : after second()n"); // Does not print.
}
int main(int argc, const char * argv[])
{
atexit(third_1); // register first handler
atexit(third_2); // register second handler
first(-3.0f);
return EXIT_SUCCESS;
}
Вывод:
first : before second()
second : before exit()
Error: -3.0 is beyond the range [-1.0; 1.0]
Abort trap: 6
Вывод в отладчике:
$ lldb abort_example
(lldb) target create "abort_example"
Current executable set to 'abort_example' (x86_64).
(lldb) run
Process 22570 launched: '/Users/ariel/abort_example' (x86_64)
first : before second()
second : before exit()
Error: -3.0 is beyond the range [-1.0; 1.0]
Process 22570 stopped
* thread #1: tid = 0x113a8, 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`__pthread_kill:
-> 0x7fff89c01286 <+10>: jae 0x7fff89c01290 ; <+20>
0x7fff89c01288 <+12>: movq %rax, %rdi
0x7fff89c0128b <+15>: jmp 0x7fff89bfcc53 ; cerror_nocancel
0x7fff89c01290 <+20>: retq
(lldb)
В случае критической ошибки нужно использовать функцию abort()
. К примеру, если при выделении памяти или записи файла произошла ошибка. Любые дальнейшие действия могут усугубить ситуацию. Если завершить выполнение обычным способом, при котором производится сброс потоков ввода — вывода, можно потерять ещё неповрежденные данные и временные файлы, поэтому самым лучшим решением будет записать дамп и мгновенно завершить программу.
В случае же некритической ошибки, например, вы не смогли открыть файл, можно безопасно выйти через exit()
.
Функции setjmp() и longjmp()
Вот мы и подошли к самому интересному – функциям нелокальных переходов. setjmp()
и longjmp()
работают по принципу goto, но в отличие от него позволяют перепрыгивать из одного места в другое в пределах всей программы, а не одной функции.
<setjmp.h>
int setjmp(jmp_buf env);
Сохраняет информацию о контексте выполнения программы (регистры микропроцессора и прочее) в env
. Возвращает 0
, если была вызвана напрямую или value
, если из longjmp()
.
void longjmp(jmp_buf env, int value);
Восстанавливает контекст выполнения программы из env
, возвращает управление setjmp()
и передаёт ей value
.
Пример:
/*
// main.c
// setjmp simple
//
// Created by Ariel Feinerman on 18/02/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void)
{
printf("second : before longjmp()n"); // prints
longjmp(buf, 1); // jumps back to where setjmp was called – making setjmp now return 1
printf("second : after longjmp()n"); // does not prints
// <- Here is the point that is never reached. All impossible cases like your own house in Miami, your million dollars, your nice girl, etc.
}
void first(void)
{
printf("first : before second()n");
second();
printf("first : after second()n"); // does not print
}
int main(int argc, const char * argv[])
{
if (!setjmp(buf))
first(); // when executed, setjmp returned 0
else // when longjmp jumps back, setjmp returns 1
printf("mainn"); // prints
return EXIT_SUCCESS;
}
Вывод:
first : before second()
second : before longjmp()
main
Используя setjmp()
и longjmp
() можно реализовать механизм исключений. Во многих языках высокого уровня (например, в Perl) исключения реализованы через них.
Пример:
/*
// main.c
// exception simple
//
// Created by Ariel Feinerman on 18/02/17.
// Copyright 2017 Feinerman Research, Inc. All rights reserved.
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <setjmp.h>
#define str(s) #s
static jmp_buf buf;
typedef enum {
NO_EXCEPTION = 0,
RANGE_EXCEPTION = 1,
NUM_EXCEPTIONS
} exception_t;
static char *exception_name[NUM_EXCEPTIONS] = {
str(NO_EXCEPTION),
str(RANGE_EXCEPTION)
};
float asin_e(float num)
{
if ((num < 1.0f) && (num > -1.0f)) {
return asinf(num);
}
else {
longjmp(buf, RANGE_EXCEPTION); // | @throw
}
}
void do_work(float num)
{
float res = asin_e(num);
printf("asin(%f) = %fn", num, res);
}
int main(int argc, const char * argv[])
{
exception_t exc = NO_EXCEPTION;
if (!(exc = setjmp(buf))) { // |
do_work(-3.0f); // | @try
} // |
else { // |
fprintf(stderr, "%s was hadled in %s()n", exception_name[exc], __func__); // | @catch
} // |
return EXIT_SUCCESS;
}
Вывод:
RANGE_EXCEPTION was hadled in main()
Внимание! Функции setjmp()
и longjmp
() в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вверх по стеку – в вызывающую функцию, а в параллельную, уже завершившую выполнение?
Информация
- стандарт ISO/IEC C (89/99/11)
- Single UNIX Specifcation, Version 4, 2016 Edition
- The Open Group Base Specifcations Issue 7, 2016 Edition (POSIX.1-2008)
- SEI CERT C Coding Standard
- cправочная информация среды программирования
- справочная информация операционной системы (man pages)
- заголовочные файлы (/usr/include)
- исходные тексты библиотеки (C Standard Library)
How can I show message boxes with a «Ding!» sound and a red ‘close’ button in it? This is what I’m talking about:
I’m trying to create some custom errors and warnings, but this:
MessageBox.Show("asdf");
doesn’t seem to give me any customization options.
TylerH
20.7k65 gold badges73 silver badges98 bronze badges
asked Jan 21, 2010 at 13:21
Try this:
MessageBox.Show("Some text", "Some title",
MessageBoxButtons.OK, MessageBoxIcon.Error);
caiosm1005
1,6761 gold badge19 silver badges31 bronze badges
answered Jan 21, 2010 at 13:23
Andrew HareAndrew Hare
343k71 gold badges638 silver badges634 bronze badges
2
Try details: use any option:
MessageBox.Show(
"your message",
"window title",
MessageBoxButtons.OK,
MessageBoxIcon.Warning // for Warning
//MessageBoxIcon.Error // for Error
//MessageBoxIcon.Information // for Information
//MessageBoxIcon.Question // for Question
);
TylerH
20.7k65 gold badges73 silver badges98 bronze badges
answered Jun 30, 2015 at 12:21
MessageBox.Show(
"your message",
"window title",
MessageBoxButtons.OK,
MessageBoxIcon.Asterisk //For Info Asterisk
MessageBoxIcon.Exclamation //For triangle Warning
)
Antonio
19.3k12 gold badges98 silver badges195 bronze badges
answered Jul 8, 2015 at 12:35
You should add namespace if you are not using it:
System.Windows.Forms.MessageBox.Show("Some text", "Some title",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Error);
Alternatively, you can add at the begining of your file:
using System.Windows.Forms
and then use (as stated in previous answers):
MessageBox.Show("Some text", "Some title",
MessageBoxButtons.OK, MessageBoxIcon.Error);
answered Jun 28, 2018 at 14:15
TidesTides
11111 bronze badges
Clang solves that by having their diagnostics have operator<<
overloaded, streaming the required arguments, which I adopted in my compiler too
DiagnosticBuilder Error( ErrType type, string msg, int line );
You can then call it like
Error(Serious, "Variable % is not known", lineNumber) << var;
When the diagnostic-builder’s destructor is called, the error is emitted.
struct DiagnosticBuilder {
DiagnosticBuilder(std::string const& format)
:m_emit(true), m_format(format)
{ }
DiagnosticBuilder(DiagnosticBuilder const& other)
:m_emit(true), m_format(other.m_format), m_args(other.m_args) {
other.m_emit = false;
}
~DiagnosticBuilder() {
if(m_emit) {
/* iterate over m_format, and print the next arg
everytime you hit '%' */
}
}
DiagnosticBuilder &operator<<(string const& s) {
m_args.push_back(s);
return *this;
}
DiagnosticBuilder &operator<<(int n) {
std::ostringstream oss; oss << n;
m_args.push_back(oss.str());
return *this;
}
// ...
private:
mutable bool m_emit;
std::string m_format;
std::vector<std::string> m_args;
};
I find that this way of doing it is quite convenient. You can extend it to support multiple languages by numbering the argument placeholders, like "Variable %2 isn't found in scope %1"
.
Содержание
- Исключения (Exceptions) и инструкция try
- Оговорка catch
- Блок finally
- Инструкция using
- Выбрасывание исключений
- Основные свойства System.Exception
- Основные типы исключений
- Директивы препроцессора
- Pragma Warning
- Атрибут Conditional
- Классы Debug и Trace
- TraceListener
- Fail и Assert
Исключения, их обработка, и некоторые другие моменты, связанные с ошибками в приложении на C#.
Исключения (Exceptions) и инструкция try
Инструкция try
отмечает блок кода как объект для обработки ошибок или очистки. После блока try
обязательно должен идти либо блок catch
, либо блок finally
, либо они оба. Блок catch
выполняется, когда внутри блока try возникает ошибка. Блок finally
выполняется после того, как прекращает выполнять блок try
(или, если присутствует, блок catch
), независимо от того, выполнился ли он до конца или был прерван ошибкой, что позволяет выполнить так называемый код очистки.
Блок catch
имеет доступ к объекту исключения (Exception
), который содержит информацию об ошибке. Блок catch
позволяет обработать исключительную ситуацию и как-либо скорректировать ошибку или выбросить новое исключение. Повторное выбрасывание исключения в блоке catch
обычно применяется с целью логирования ошибок или чтобы выбросить новое, более специфическое исключение.
Блок finally
добавляет в программу прогнозируемость, позволяя выполнить определенный код при любых обстоятельствах. Это может быть полезно для выполнения операций очистки, например, закрытия сетевого подключения и т.д.
В целом конструкция try выглядит следующим образом:
try { ... // в пределах этого блока может быть выброшено исключение } catch (ExceptionA ex) { ... // обработчик исключений типа ExceptionA } catch (ExceptionB ex) { ... // обработчик исключений типа ExceptionB } finally { ... // код очистки } |
Например, следующий код выбросит ошибку DivideByZeroException
(поскольку делить на ноль нельзя) и наша программа завершить досрочно:
int x = 3, y = 0; Console.WriteLine (x / y); |
Чтобы этого избежать можно использовать конструкцию try
:
try { int x = 3, y = 0; Console.WriteLine (x / y); } catch (DivideByZeroException ex) { Console.Write («y cannot be zero. «); } // выполнение программы продолжится отсюда |
Обработка исключений довольно ресурсоёмкая операция, поэтому на практике для таких случаев как в примере ее лучше не использовать (лучше непосредственно перед делением проверить делить на равенство нулю).
Когда выбрасывается исключение, CLR проверяет выброшено ли оно непосредственно внутри блока try
, который может обработать данное исключение. Если да, выполнение переходит в соответствующий блок catch
. Если блок catch
успешно завершается, выполнение переходит к следующей после блока try
инструкции (если имеется блок finally
, то сначала выполняется он). Если же исключение выброшено не внутри блока try
или конструкция try
не содержит соответствующего блока catch
, выполнение переходит в точку вызова метода (при этом сначала выполняется блок finally
), и проверка повторяется снова.
Если не одна функция в стэке вызовов не способна обработать исключение, ошибка выводиться пользователю и программа завершается досрочно.
Оговорка catch
В оговорке catch указывается какой тип исключения она должна перехватывать. Это может быть либо System.Exception
, либо его производный класс. Перехватывая непосредственно System.Exception
, мы перехватим все возможные ошибки. Это может быть полезно в нескольких случаях:
- программа потенциально должна и может продолжить работать несмотря на ошибки любых типов
- исключение будет выброшено повторно в блоке
catch
, например, после логирования ошибок - блок
catch
является последним в очереди, способным предотвратить аварийное завершение программы
Однако обычно перехватываются исключения более специфического типа, чтобы избежать ситуации, когда обработчику ошибки придется иметь дело с исключением, для которого он не предназначен (например, OutOfMemoryException
).
Можно обработать несколько типов исключений с помощью нескольких оговорок catch:
try { DoSomething(); } catch (IndexOutOfRangeException ex) { ... } catch (FormatException ex) { ... } catch (OverflowException ex) { ... } |
Каждая оговорка способна обработать только то исключение, которое точно совпадает с ее типом. Для одного выброшенного исключения может быть выполнена только одна оговорка catch. Обрабатываются блоки catch в том порядке, в котором они идут в коде. В этой связи более специфические исключения должны перехватываться раньше чем более общие.
Исключение может быть перехвачено и без указания переменной, если не нужен доступ к ее членам:
catch (StackOverflowException) // без переменной { ... } |
Более того, в оговорке catch можно опустить и переменную и тип исключения — такая оговрка будет перехватывать все исключения:
Блок finally
Блок finally
выполняется всегда, независимо от того выброшено исключение или нет. Блок finally
обычно содержит код очистки.
Блок finally
выполняется в следующих случаях:
- после завершения блока
catch
- если выполнение блока
try
прервано jump-инструкциями:return
,goto
и т.д. - после выполнения блока
try
полностью, если исключений так и не было выброшено
Блок finally
делает программу более прогнозируемой. Например, в следующем примере открываемый файл в итоге всегда будет закрыт, независимо от того, завершиться ли блок try
без ошибок, или будет прерван выброшенным исключением, или сработает инструкция return
если файл окажется пустым:
static void ReadFile() { StreamReader reader = null; try { reader = File.OpenText («file.txt»); if (reader.EndOfStream) return; Console.WriteLine (reader.ReadToEnd()); } finally { if (reader != null) reader.Dispose(); } } |
В пример для закрытия файла вызывается метод Dispose
. Использование этого метода внутри блока finally
является стандартной практикой. C# даже позволяет заменить всю конструкцию инструкцией using
.
Инструкция using
Многие классы инкапсулируют неуправляемые ресурсы, такие как дескриптор файла, соединение с базой данных и т.д. Эти классы реализуют интерфейс System.IDisposable
, который содержит единственный метод без параметров Dispose
, освобождающий соответствующие машинные ресурсы. Инструкция using
предусматривает удобный синтаксис вызова метода Dispose
для объектов реализующих IDisposable
внутри блока finally
:
using (StreamReader reader = File.OpenText («file.txt»)) { ... } |
Что эквивалентно следующей конструкции:
StreamReader reader = File.OpenText («file.txt»); try { ... } finally { if (reader != null) ((IDisposable)reader).Dispose(); } |
Выбрасывание исключений
Исключение может быть выброшено автоматически во время выполнения программы либо явно в коде программы с помощью ключевого слова throw
:
static void Display (string name) { if (name == null) throw new ArgumentNullException («name»); Console.WriteLine (name); } |
Также исключение может быть выброшено повторно внутри блока catch
:
try { ... } catch (Exception ex) { // логирование ошибки ... throw; // повторное выбрасывание того же самого исключения } |
Такой подход позволяет заносить ошибки в лог без их дальнейшего поглощения. Также это позволяет уклониться от обработки неожиданных исключений.
Если throw
заменить на throw ex
, то пример по прежнему будет работать, но свойство исключения StackTrace
не будет отражать исходную ошибку.
Другой распространенный сценарий использования повторного выбрасывания исключения — повторное выбрасывание более специфического и конкретного типа исключения, чем было перехвачено ранее:
try { ... // парсинг даты рождения из xml-данных } catch (FormatException ex) { throw new XmlException («Неправильная дата рождения», ex); } |
В таких случаях необходимо передать исходное исключение в качестве первого параметра конструктора нового исключения, ссылка на объект исходного исключения позже будет доступна через свойство InnerException
внутреннего исключения.
Основные свойства System.Exception
К наиболее важным свойствам класса System.Exception
можно отнести:
StackTrace
— строка, представляющая все методы, которые были вызваны, начиная с того, в котором было выброшено исключение, и заканчивая тем, в котором содержится блокcatch
, перехвативший исключение;Message
— строка с описанием ошибки;InnerException
— содержит ссылку на объектExeption
, который вызвал текущее исключение (например, при повторном выбрасывании исключения).
Основные типы исключений
Следующие типы исключений являются наиболее распространенными в среде CLR и .NET Framework. Их можно выбрасывать непосредственно или использовать как базовые классы для пользовательских типов исключений.
System.ArgumentException
— выбрасывается при вызове функции с неправильным аргументом.System.ArgumentNullException
— производный отArgumentException
класс, выбрасывается если один из аргументов функции неожиданно равенnull
.System.ArgumentOutOfRangeException
— производный отArgumentException
класс, выбрасывается когда аргумент функции имеет слишком большое или слишком маленькое значение для данного типа (обычно касается числовых типов). Например, такое исключение будет выброшено если попытаться передать отрицательное число в функцию, которая ожидает только положительные числа.System.InvalidOperationException
— выбрасывается когда состояние объекта является неподходящим для нормального выполнения метода, например, при попытке прочесть не открытый файл.System.NotSupportedException
— выбрасывается, когда запрошенный функционал не поддерживается, например, если попытаться вызвать методAdd
для коллекции доступной только для чтения (свойство коллекцииIsReadOnly
возвращаетtrue
).System.NotImplementedException
— выбрасывается, когда запрошенный функционал еще не реализован.System.ObjectDisposedException
— выбрасывается при попытке вызвать метод объекта, который уже был уничтожен (disposed).
Директивы препроцессора
Директивы препроцессора снабжают компилятор дополнительной информацией об областях кода. Самые распространенные директивы препроцессора — условные директивы, позволяющие включить или исключить области кода из компиляции.
#define DEBUG class MyClass { int x; void Foo() { # if DEBUG Console.WriteLine («Testing: x = {0}», x); # endif } } |
В этом классе инструкции в методе Foo
скомпилируются если определен символ DEBUG
, а если его удалить — инструкции не скомпилируются. Символы препроцессора могут быть определены в исходном коде (как в примере), а могут быть переданы компилятору в командной строке с помощью параметра /define:symbol
.
С директивами #if
и #elif
можно использовать операторы ||
, &&
и !
с несколькими символами:
Директивы #error
и #warning
предотвращают некорректное использование условных директив, заставляя компилятор генерировать предупреждение или ошибку при передаче неверного набора символов.
Директивы препроцессора схожи с условными конструкциями и статическими переменными, однако дают возможности, недоступные для последних:
- условное включение атрибута
- изменение типа, объявляемого для переменной
- переключение между разными пространствами имен или псевдонимами типа в директиве using:
using TestType =
#if V2
MyCompany.Widgets.GadgetV2;
#else
MyCompany.Widgets.Gadget;
#endif
- создавать новые версии кода и быстро переключаться между ними при компиляции
- создавать библиотеки, компилируемые для разных версий .NET Framework
Полный список директив препроцессора:
#define symbol
— определяет символ#undef symbol
— удаляет символ#if symbol [оператор symbol2]...
— условная компиляция; допустимые операторы==
,!=
,&&
и||
#else
— выполняет код после#endif
#elif symbol [оператор symbol2]
— объединяет#else
и#if
#endif
— конец условных директив#warning text
— текст предупреждения, которое появится в выдаче компилятора#error text
— текст ошибки, которая появится в выдаче компилятора#line [число["файл"] | hidden]
— число указывает номер строки в исходном коде; файл — имя файла, которое появится в выдаче компилятора; hidden — дает указание дебагеру пропустить код от этой точки до следующей директивы#line
#region name
— отмечает начало области#endregion
— отмечает конец области#pragma warning
Pragma Warning
Компилятор генерирует предупреждения, когда что-то в коде ему кажется неуместным (но корректным). В отличии от ошибок предупреждения не препятствуют компиляции программы. Предупреждения компилятора могут быть очень полезны при поиске багов в программе. Однако часто предупреждения оказываются ложными, поэтому целесообразно иметь возможность получать предупреждения только о действительных багах. С этой целью компилятор дает возможность выборочно подавить предупреждения с помощью директивы #pragma warning
.
public class Foo { static void Main() { } #pragma warning disable 414 static string Message = «Hello»; #pragma warning restore 414 } |
В примере мы указываем компилятору не выдавать предупреждения о том, что поле Message
не используется.
Если не указывать номер директива #pragma warning
отменит или восстановит вывод всех предупреждений.
Если скомпилировать программу с параметром /warnaserror
, то все не отмененные директивой #pragma warning
предупреждения будут расцениваться компилятором как ошибки.
Атрибут Conditional
Атрибут Conditional
указывает компилятору на необходимость игнорировать все обращения к определенному классу или методу, если заданный символ не был определен:
[Conditional («LOGGINGMODE»)] static void LogStatus (string msg) { ... } |
Это равносильно тому, что каждый вызов метода будет окружен условными директивами:
#if LOGGINGMODE LogStatus («Message Headers: « + GetMsgHeaders()); #endif |
Классы Debug и Trace
Статические классы Debug
и Trace
предлагают базовые возможности логирования. Оба класса схожи, отличие заключается в их назанчении. Класс Debug
предназначен для отладочных сборок, класс Trace
— для отладочных и финальных. В связи с этим все методы класса Debug
определены с атрибутом [Conditional("DEBUG")]
, а методы класса Trace
— с атрибутом [Conditional("TRACE")]
. Это значит, что все обращения к Debug
и Trace
будут подавляться компилятором, пока не определен символ DEBUG
или TRACE
.
Класс Debug
и Trace
определяют методы Write
, WriteLine
и WriteIf
. По умолчанию они отправляют сообщения в окно вывода отладчика:
Debug.Write («Data»); Debug.WriteLine (23 * 34); int x = 5, y = 3; Debug.WriteIf (x > y, «x is greater than y»); |
Класс Trace
также содержит методы TraceInformation
, TraceWarning
и TraceError
. Их действия зависят от зарегистрированных прослушивателей.
TraceListener
Классы Debug
и Trace
имеют свойство Listeners
, которое представляет собой статическую коллекцию экземпляров TraceListener
. Они отвечают за обработку данных, возвращаемых методами Write
, Fail
и Trace
.
По умолчанию коллекция Listeners
обоих классов включает единственный прослушиватель — DefaultTraceListener
— стандартный прослушиватель, имеющий две ключевые возможности:
- при подключении к отладчику (например, Visual Studio) сообщения записываются в окно вывода отладчика, во всех остальных случаях сообщения игнорируются
- при вызове метода
Fail
отображается диалоговое окно, запрашивающее у пользователя дальнейшие действия: продолжить, прервать или повторить отладку (независимо от того, подключен ли отладчик)
Это поведение можно изменить или дополнить, удалив (на обязательно) стандартный прослушиватель и/или добавив один или более собственных прослушивателей.
Прослушиваетли трассировки можно написать с нуля (создав производный класс от TraceListener
) или воспользоваться готовыми классами:
TextWriterTraceListener
записывает вStream
илиTextWriter
или добавляет в файл; имеет четыре подкласса:ConsoleTraceListener
,DelimitedListTraceListener
,XmlWriterTraceListener
иEventSchemaTraceListener
EventLogTraceListener
записывает в журнал событий WindowsEventProviderTraceListener
записывает в систему трассировки событий Windows (Event Tracing for Windows — ETW)WebPageTraceListener
выводит на веб-страницу ASP.NET
Ни один из этих прослушивателе не отображает диалоговое окно при вызове Fail
, это делает только DefaultTraceListener
.
// Удалить стандартный прослушиватель, очистив коллекцию прослушивателей: Trace.Listeners.Clear(); // Добавить средство записи в файл trace.txt: Trace.Listeners.Add (new TextWriterTraceListener («trace.txt»)); // Добавит средство записи в консоль: System.IO.TextWriter tw = Console.Out; Trace.Listeners.Add (new TextWriterTraceListener (tw)); // Добавить средство записи в журнал событий Windows: if (!EventLog.SourceExists («DemoApp»)) EventLog.CreateEventSource («DemoApp», «Application»); Trace.Listeners.Add (new EventLogTraceListener («DemoApp»)); |
В случае журнала событий Windows сообщения, отправляемые с помощью Write
, Fail
или Assert
, записываются как сведения, а сообщения методов TraceWarning
и TraceError
записываются как предупреждения или ошибки.
Каждый экземпляр TraceListener
имеет свойство Filter
и TraceFilter
, с помощью которых можно управлять, будет ли сообщение записано в этот прослушиватель. Для этого необходимо создать экземпляр классов EventTypeFilter
или SourceFilter
(производных от TraceFilter
) или создать свой класс, наследующий от TraceFilter
и переопределить в нем метод ShouldTrace
.
В TraceListener
также определены свойства IndentLevel
и IndentSize
для управления отступами и свойство TraceOutputOptions
для записи дополнительных данных:
TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out); tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack; // Это применяется при использовании метода Trace: Trace.TraceWarning («Orange alert»); DiagTest.vshost.exe Warning: 0 : Orange alert DateTime=2007—03—08T05:57:13.6250000Z Callstack= at System.Environment.GetStackTrace(Exception e, Boolean needFileInfo) at System.Environment.get_StackTrace() at ... |
Прослушиватели, которые записывают данные в поток, кэшируются. По этой причине данные не появляются в потоке немедленно, а также поток перед завершением приложения должен быть закрыт, или хотя бы сброшен, чтоб не потерять данные в кэше. Для этой цели классы Trace
и Debug
содержат статические методы Close
и Flush
, которые вызывают Close
и Flush
во всех прослушивателях (а они в свою очередь закрывают или сбрасывают все потоки). Метод Close
вызывает метод Flush
, закрывает файловые дескрипторы и предотвращает дальнейшую запись.
Классы Trace
и Debug
также определяют свойство AutoFlush
, которое если равно true
вызывает Flush
после каждого сообщения.
Fail и Assert
Классы Debug
и Trace
содержат методы Fail
и Assert
.
Метод Fail
отправляет сообщения каждому TraceListener
:
Debug.Fail («File data.txt does not exist!»); |
Метод Assert
вызывает Fail
если аргумент типа bool
равен false
. Это называется созданием утверждения и указывает на ошибку, если оно нарушено. Можно также создать необязательное сообщение об ошибке:
Debug.Assert (File.Exists («data.txt»), «File data.txt does not exist!»); var result = ... Debug.Assert (result != null); |
Методы Write
, Fail
и Assert
также могут принимать категорию в виде строки ,которая может быть использована при обработке вывода.
В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.
Некоторые ошибки времени выполнения можно обнаружить заранее с помощью проверок в коде. Например, такими могут быть ошибки, нарушающие инвариант класса в конструкторе. Обычно, если ошибка обнаружена, то дальнейшее выполение функции не имеет смысла, и нужно сообщить об ошибке в то место кода, откуда эта функция была вызвана. Для этого предназначен механизм исключений.
Коды возврата и исключения
Рассмотрим функцию, которая считывает со стандартного потока возраст и возвращает его вызывающей стороне. Добавим в функцию проверку корректности возраста: он должен находиться в диапазоне от 0 до 128 лет. Предположим, что повторный ввод возраста в случае ошибки не предусмотрен.
int ReadAge() {
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
// Что вернуть в этом случае?
}
return age;
}
Что вернуть в случае некорректного возраста? Можно было бы, например, договориться, что в этом случае функция возвращает ноль. Но тогда похожая проверка должна быть и в месте вызова функции:
int main() {
if (int age = ReadAge(); age == 0) {
// Произошла ошибка
} else {
// Работаем с возрастом age
}
}
Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main
. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.
Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:
#include <iostream>
struct WrongAgeException {
int age;
};
int ReadAge() {
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
throw WrongAgeException(age);
}
return age;
}
Здесь в случае ошибки оператор throw
генерирует исключение, которое представлено временным объектом типа WrongAgeException
. В этом объекте сохранён для контекста текущий неправильный возраст age
. Функция досрочно завершает работу: у неё нет возможности обработать эту ошибку, и она должна сообщить о ней наружу. Поток управления возвращается в то место, откуда функция была вызвана. Там исключение может быть перехвачено и обработано.
Перехват исключения
Мы вызывали нашу функцию ReadAge
из функции main
. Обработать ошибку в месте вызова можно с помощью блока try
/catch
:
int main() {
try {
age = ReadAge(); // может сгенерировать исключение
// Работаем с возрастом age
} catch (const WrongAgeException& ex) { // ловим объект исключения
std::cerr << "Age is not correct: " << ex.age << "n";
return 1; // выходим из функции main с ненулевым кодом возврата
}
// ...
}
Мы знаем заранее, что функция ReadAge
может сгенерировать исключение типа WrongAgeException
. Поэтому мы оборачиваем вызов этой функции в блок try
. Если происходит исключение, для него подбирается подходящий catch
-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main
, то программа аварийно завершается.
Усложним немного наш пример, чтобы из функции ReadAge
могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age
никак не изменит переменную age
, а лишь возведёт специальный флаг ошибки в объекте std::cin
. Наша переменная age
останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin
, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:
int ReadAge() {
std::cin.exceptions(std::istream::failbit);
int age;
std::cin >> age;
if (age < 0 || age >= 128) {
throw WrongAgeException(age);
}
return age;
}
Теперь ошибка чтения в операторе >>
у потока ввода будет приводить к исключению типа std::istream::failure
. Функция ReadAge
его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main
:
int main() {
try {
age = ReadAge(); // может сгенерировать исключения разных типов
// Работаем с возрастом age
} catch (const WrongAgeException& ex) {
std::cerr << "Age is not correct: " << ex.age << "n";
return 1;
} catch (const std::istream::failure& ex) {
std::cerr << "Failed to read age: " << ex.what() << "n";
return 1;
} catch (...) {
std::cerr << "Some other exceptionn";
return 1;
}
// ...
}
При обработке мы воспользовались функцией ex.what
у исключения типа std::istream::failure
. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.
Обратите внимание на третий catch
с многоточием. Такой блок, если он присутствует, будет перехватывать любые исключения, не перехваченные ранее.
Исключения стандартной библиотеки
Функции и классы стандартной библиотеки в некоторых ситуациях генерируют исключения особых типов. Все такие типы выстроены в иерархию наследования от базового класса std::exception
. Иерархия классов позволяет писать обработчик catch
сразу на группу ошибок, которые представлены базовым классом: std::logic_error
, std::runtime_error
и т. д.
Вот несколько примеров:
-
Функция
at
у контейнеровstd::array
,std::vector
иstd::deque
генерирует исключениеstd::out_of_range
при некорректном индексе. -
Аналогично, функция
at
уstd::map
,std::unordered_map
и у соответствующих мультиконтейнеров генерирует исключениеstd::out_of_range
при отсутствующем ключе. -
Обращение к значению у пустого объекта
std::optional
приводит к исключениюstd::bad_optional_access
. -
Потоки ввода-вывода могут генерировать исключение
std::ios_base::failure
.
Исключения в конструкторах
В главе 3.1 мы написали класс Time
. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time
передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.
Более правильным было бы сгенерировать в конструкторе исключение. Таким образом мы бы явно передали сообщение об ошибке во внешнюю функцию, которая пыталась создать объект.
class Time {
private:
int hours, minutes, seconds;
public:
// Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
class IncorrectTimeException {
};
Time::Time(int h, int m, int s) {
if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
throw IncorrectTimeException();
}
hours = h;
minutes = m;
seconds = s;
}
// ...
};
Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.
Свёртка стека
Вспомним класс Logger
из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception
, чтобы не писать свой класс исключения.
#include <exception>
#include <iostream>
void f() {
std::cout << "Welcome to f()!n";
Logger x;
// ...
throw std::exception(); // в какой-то момент происходит исключение
}
int main() {
try {
Logger y;
f();
} catch (const std::exception&) {
std::cout << "Something happened...n";
return 1;
}
}
Мы увидим такой вывод:
Logger(): 1 Welcome to f()! Logger(): 2 ~Logger(): 2 ~Logger(): 1 Something happened...
Сначала создаётся объект y
в блоке try
. Затем мы входим в функцию f
. В ней создаётся объект x
. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try
, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x
, а затем — объекта y
.
Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger
и сгенерируем нарочно исключение в его конструкторе:
#include <exception>
#include <iostream>
class C {
private:
Logger x;
public:
C() {
std::cout << "C()n";
Logger y;
// ...
throw std::exception();
}
~C() {
std::cout << "~C()n";
}
};
int main() {
try {
C c;
} catch (const std::exception&) {
std::cout << "Something happened...n";
}
}
Вывод программы:
Logger(): 1 // конструктор поля x C() Logger(): 2 // конструктор локальной переменной y ~Logger(): 2 // свёртка стека: деструктор y ~Logger(): 1 // свёртка стека: деструктор поля x Something happened...
Заметим, что деструктор самого класса C
не вызывается, так как объект в конструкторе не был создан.
Механизм свёртки стека гарантирует, что деструкторы для всех созданных автоматических объектов или полей класса в любом случае будут вызваны. Однако он полагается на важное свойство: деструкторы самих классов не должны генерировать исключений. Если исключение в деструкторе произойдёт в момент свёртки стека при обработке другого исключения, то программа аварийно завершится.
Пример с динамической памятью
Подчеркнём, что свёртка стека работает только с автоматическими объектами. В этом нет ничего удивительного: ведь за временем жизни объектов, созданных в динамической памяти, программист должен следить самостоятельно. Исключения вносят дополнительные сложности в ручное управление динамическими объектами:
void f() {
Logger* ptr = new Logger(); // конструируем объект класса Logger в динамической памяти
// ...
g(); // вызываем какую-то функцию
// ...
delete ptr; // вызываем деструктор и очищаем динамическую память
}
На первый взгляд кажется, что в этом коде нет ничего опасного: delete
вызывается в конце функции. Однако функция g
может сгенерировать исключение. Мы не перехватываем его в нашей функции f
. Механизм свёртки уберёт со стека лишь сам указатель ptr
, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger
, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:
void f() {
Logger* ptr = new Logger();
// ...
try {
g();
} catch (...) { // ловим любое исключение
delete ptr; // вручную удаляем объект
throw; // перекидываем объект исключения дальше
}
// ...
delete ptr;
}
Здесь мы перехватываем любое исключение и частично обрабатываем его, удаляя объект в динамической памяти. Затем мы прокидываем текущий объект исключения дальше с помощью оператора throw
без аргументов.
Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try
/catch
любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.
Гарантии безопасности исключений
Предположим, что мы пишем свой класс-контейнер, похожий на двусвязный список. Наш контейнер позволяет добавлять элементы в хранилище и отдельно хранит количество элементов в некотором поле elementsCount
. Один из инвариантов этого класса такой: значение elementsCount
равно реальному числу элементов в хранилище.
Не вдаваясь в детали, давайте посмотрим, как могла бы выглядеть функция добавления элемента.
template <typename T>
class List {
private:
struct Node { // узел двусвязного списка
T element;
Node* prev = nullptr; // предыдущий узел
Node* next = nullptr; // следующий узел
};
Node* first = nullptr; // первый узел списка
Node* last = nullptr; // последний узел списка
int elementsCount = 0;
public:
// ...
size_t Size() const {
return elementsCount;
}
void PushBack(const T& elem) {
++elementsCount;
// Конструируем в динамической памяти новой узел списка
Node* node = new Node(elem, last, nullptr);
// Связываем новый узел с остальными узлами
if (last != nullptr) {
last->next = node;
} else {
first = node;
}
last = node;
}
};
Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack
. В ней могут произойти такие исключения:
-
Выражение
new
может сгенерировать исключениеstd::bad_alloc
из-за нехватки памяти. -
Конструктор копирования класса
T
может сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поляelement
создаваемого узла в конструкторе классаNode
. В этом случаеnew
ведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.
Эти исключения не перехватываются в функции PushBack
. Их может перехватить код, из которого PushBack
вызывался:
#include <iostream>
class C; // какой-то класс
int main() {
List<C> data;
C element;
try {
data.PushBack(element);
} catch (...) { // не получилось добавить элемент
std::cout << data.Size() << "n"; // внезапно 1, а не 0
}
// работаем дальше с data
}
Наша функция PushBack
сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List
нарушается инвариант: значение счётчика elementsCount
перестаёт соответствовать реальности. Можно сказать, что функция PushBack
не даёт гарантий безопасности.
Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):
-
Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений. Примерами могут служить правильно написанные деструктор и конструктор перемещения, а также константные функции вида
Size
. -
Строгая гарантия безопасности. Исключение может возникнуть, но от этого объект нашего класса не поменяет состояние: количество элементов останется прежним, итераторы и ссылки не будут инвалидированы и т. д.
-
Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.
-
Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.
Всегда стоит разрабатывать классы, обеспечивающие хотя бы базовую гарантию безопасности. При этом не всегда возможно эффективно обеспечить строгую гарантию.
Переместим в нашей функции PushBack
изменение счётчика в конец:
void PushBack(const T& elem) {
Node* node = new Node(elem, last, nullptr);
if (last != nullptr) {
last->next = node;
} else {
first = node;
}
last = node;
++elementsCount; // выполнится только если раньше не было исключений
}
Теперь такая функция соответствует строгой гарантии безопасности.
В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector
.
-
Деструктор, функции
empty
,size
,capacity
, а такжеclear
предоставляют гарантию отсутствия сбоев. -
Функции
push_back
иresize
предоставляют строгую гарантию. -
Функция
insert
предоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.
Функции класса, которые гарантируют отсутсвие сбоев, следует помечать ключевым словом noexcept
:
class C {
public:
void f() noexcept {
// ...
}
};
С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C>
при реаллокации будет использовать конструктор перемещения класса C
, если он помечен как noexcept
. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.