Введение


Ошибки, увы, неизбежны, поэтому их обработка занимает очень важное место в программировании. И если алгоритмические ошибки можно выявить и исправить во время написания и тестирования программы, то ошибок времени выполнения избежать нельзя в принципе. Сегодня мы рассмотрим функции стандартной библиотеки (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   %s\n", $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 buffer\n");
    else if (errno == EILSEQ)  
        fprintf(stderr, " Error : input conversion stopped due to an input byte that does not belong to the input codeset\n");
    else if (errno == EINVAL)  
        fprintf(stderr, " Error : input conversion stopped due to an incomplete character or shift sequence at the end of the input buffer\n");
/* 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 : %s\n", 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 : %s\n", 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 : %s\n", 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, %d\n", error);
                    break;
            case ERANGE:
                    fprintf (stderr, "strerror_r() failed: buffer too small: %d\n", MSG_LEN);
            case 0:
                    fprintf(stderr, "Error message : %s\n", error_buf);
                    break;
            default: 
                    fprintf (stderr, "strerror_r() failed: unknown error, %d\n", 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 : %s\nLine : %d\nCurrent function : %s()\nFailed function : %s()\nError message : %s\n", __FILE__, __LINE__, __func__, "fopen", error_buf);
	                    break;
                    case ERANGE: 
                            error_len *= MUL_FACTOR;
                            break;
                    case EINVAL: 
                            fprintf (stderr, "strerror_r() failed: invalid error code, %d\n", error_num);
                            break;
                    default:
                            fprintf (stderr, "strerror_r() failed: unknown error, %d\n", 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) = %f\n", 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) = %f\n", 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 #2\n");          // Does not print.
}

void third_1(void) 
{
    printf("third #1\n");          // Does not print.
}

void second(double num) 
{
    printf("second : before exit()\n");	// Prints.
    
    if ((num < 1.0f) && (num > -1.0f)) {
        printf("asin(%.1f) = %.3f\n", 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 #2\n");          // Does not print.
}

void third_1(void) 
{
    printf("third #1\n");          // Does not print.
}

void second(double num) 
{
    printf("second : before exit()\n");	// Prints.
    
    if ((num < 1.0f) && (num > -1.0f)) {
        printf("asin(%.1f) = %.3f\n", 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("main\n");       // 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) = %f\n", 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() в первую очередь применяются в системном программировании, и их использование в клиентском коде не рекомендуется. Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вверх по стеку – в вызывающую функцию, а в параллельную, уже завершившую выполнение?

Информация


Поделиться с друзьями
-->

Комментарии (41)


  1. 3aicheg
    03.04.2017 09:13
    +7

    «Доктор, откуда у вас такие картинки?»


    1. Casufi
      03.04.2017 09:47
      +2

      Самому интересно стало что это такое
      https://ru.wikipedia.org/wiki/Trachelophorus_giraffa


      1. 3aicheg
        03.04.2017 10:19

        Спасибо товарищу Гуголю за нашу спокойную старость (в счастливом детстве поди найди по картинке, что сие).


      1. arielf
        03.04.2017 15:00
        +1

        Видите, у него и родовое название говорящее. :3


    1. hdfan2
      03.04.2017 09:52
      +5

      Fucking bugs.


    1. arielf
      03.04.2017 13:57

      Необычные жуки символизируют процесс появления ошибок.


  1. hdfan2
    03.04.2017 09:34
    +2

    Добавлю: у замечательной функции getaddrinfo свои собственные коды ошибок, и для их перевода в человекочитаемую форму есть отдельная функция gai_strerror. Как-то раз на это налетел, поэтому запомнилось.


  1. ser-mk
    03.04.2017 14:19

    Спасибо за setjmp() и longjmp()! Не знал, было очень интересно.


  1. kloppspb
    03.04.2017 14:20

    Их применение ухудшает читаемость программы и может привести к непредсказуемым ошибкам. Например, что произойдёт, если вы прыгните не вниз по стеку – в функцию верхнего уровня, а в параллельную, уже завершившую выполнение?

    Этот вопрос решается обёртками, которых можно какое-то количество, например, на гитхабе. С их применением код на C может выглядеть так (с контролем мест прыжков и вложенности):

    try {
        throw( IOException, "Фигня какая-то случилась" );
    }
    catch( IOException ) {
    }
    catch() {
    }
    finally {
    }


    Другой вопрос — так уж ли это надо в C.


    1. arielf
      03.04.2017 14:59

      Другой вопрос — так уж ли это надо в C.

      Я считаю, что в клиентском коде (обычной юзерской проге) не нужно. Я умеренный противник исключений, ибо они прерывают обычный поток выполнения, что противоречит принципу структурного программирования. Но в системном программировании они могут быть очень полезны. Приведённый Вами пример хорош, пока у вас throw прямо в блоке try, а если оно во вложенном вызове функции и в другом файле — придётся сильнее изловчиться.


      1. kloppspb
        03.04.2017 15:28

        если оно во вложенном вызове функции и в другом файле — придётся сильнее изловчиться

        Да не такая уж это и проблема.

        Но C в этом смысле вообще консервативен. Немало подобных обёрток и библиотек делается скорей из спортивного интереса, чем из реальной необходимости. Какое-то время ими даже пользуются (в основном авторы, и я не избежал этой участи), но потом забивают и возвращаются к классике :)


        1. arielf
          03.04.2017 18:35

          Можно ссылку на репу? Интересно глянуть. Вообще проблема, ибо longjmp() не умеет прыгать 'в никуда', ей нужен конкретный буфер с сохранённой средой. Значит, эти буферы в общем случае должны быть глобальными и уникальными. И их нужно генерить неким автоматическим способом (препроцессором?). Я ещё не изучал существующие реализации, мож попробую напишу свою.


          1. kloppspb
            03.04.2017 20:32
            +1

            Идею брал отсюда. Есть ещё несколько реализаций, но они все похожи.

            В своей делал тупо: массив структур {jmp_buf + тип исключения + строка для сообщения}, и глобальный индекс текущей позиции. Соответственно, блок try заполняет текущую структуру и переставляет указатель на следующую. Блок finally() откатывает указатель, то есть обязателен в любом случае. Конечно, тут должна сохраняться последовательность обработки. В принципе тоже преодолимо, но наигрался уже на этом этапе :)


            1. arielf
              05.04.2017 20:50

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


          1. exchg
            03.04.2017 21:39

            Значит, эти буферы в общем случае должны быть глобальными и уникальными. И их нужно генерить неким автоматическим способом (препроцессором?).

            Их можно хранить в стеке вызывающей функции + глобальный указатель на предыдущий буфер.


            1. exchg
              03.04.2017 21:46

              Точнее, в стеке хранится текущий буфер и указатель на предыдущий буфер, а в в глобальной переменной хранится указатель на текущий буфер.


  1. exchg
    03.04.2017 18:27
    +2

    Наверное стоит добавить что в случае использования sjlj стоит следить за переменными (например маркировать их volatile) потому что longjmp восстанавливает контекст сохраненный в момент вызова setjmp, т.е


        void* data = allocate_some();
        if (!(exc = setjmp(buf))) {
           data = reallocate_some();
            do_work(-3.0f);
        } 
        else {
            // тут значение data МОЖЕТ быть равное значению сохраненному на момент первого вызовы setjmp
            fprintf(stderr, "%s was hadled in %s()\n", exception_name[exc], __func__);
        }  


    1. arielf
      03.04.2017 18:38

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


  1. qw1
    07.04.2017 16:44

    Вроде как немного затронута тема с C11 и кодом ошибки (например, приведена функция strerror_s).

    Стоило уделить больше внимания тому, что в C11 многие функции обзавелись вторым вариантом, который возвращает сразу и значение, и код ошибки (например, fopen и fopen_s). Примеры с функциями из C11 соседствуют с функциями C99 (в дидактических целях стоило давать все примеры с fopen_s).


    1. arielf
      07.04.2017 18:29

      Приводить все примеры из Annex K бесполезно, ибо он почти нигде не реализован, и где мы взяли код ошибки: из errno, или вернули из функции — не принципильно.


    1. arielf
      07.04.2017 18:33

      Более того, ходили слухи, что его могут вообще исключить из будущих обновлений ISO C.


      1. qw1
        07.04.2017 21:33

        Вот, а чёртова Visual Studio тр&%ет мне мозг в warning-ах, что fopen is depracated, use fopen_s.

        Хотя strcat_s и strcpy_s очень даже кстати, спасут от многих уязвимостей с переполнением буфера.


        1. arielf
          07.04.2017 21:58

          Windoпроблемы. ;-) Ни в ISO C11, ни в POSIX они не deprecated ни в коей мере. Более того, не знаю, как в новой версии VC, а ещё недавно порядок параметров в прототипы MS функций не был совместим с Annex K из C11. То есть MS продвинули свои safe функции в ISO C, но парни из комитета взяли и сделали версии функций, не совместимые вообще ни с одной существующей реализацией! В общем, такие дела.


          1. exchg
            08.04.2017 00:05

            То есть MS продвинули свои safe функции в ISO C, но парни из комитета взяли и сделали версии функций, не совместимые вообще ни с одной существующей реализацией!

            Как это они умудрились продвинуть мимо комитета, или как комитет мог перепринять? Помоему microsoft сами себе придумали эти функции и пометили старые как depricated.


            1. qw1
              08.04.2017 09:46

              особенно MS любит изобретать всякие _wfopen_s, _strncpy_l, _mbsstr, _mbstrlen и т.п.


            1. arielf
              08.04.2017 16:30

              Я свечу не держал, но вроде бы MS их придумали и предложили комитету, а они их сделали по своему.
              Подробнее:
              http://stackoverflow.com/questions/372980/do-you-use-the-tr-24731-safe-functions
              http://stackoverflow.com/questions/2169016/mac-solution-for-safe-alternatives-to-unsafe-c-c-standard-library-function
              https://msdn.microsoft.com/en-us/library/8ef0s5kh.aspx


        1. arielf
          07.04.2017 22:05
          +1

          Я привёл ссылку http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1967.htm в эссе. strcat_s() и strcpy_s() как раз не особо полезны, в C99 ввели 'n' функции: strncat() и strncpy(). Существенная разница в том, что 's' функции не используют errno, а возвращают код ошибки. Это было бы хорошо, если б не совместимость и усложение прототипов. Очень сложный вопрос, что лучше.


          1. qw1
            08.04.2017 09:53
            +1

            Ознакомился. Оказывается, strncpy делает лишнюю работу по обнулению всего буфера (а если брать с запасом char fileName[4096]; это лишнее время CPU). А strncat неинтуитивна — size это вовсе не размер destination буфера, и зануление остатка не происходит.

            Где-то была статья про разношёрстность стандартной библиотеки PHP, похоже C в ту же сторону идёт.


            1. exchg
              08.04.2017 15:43

              похоже C в ту же сторону идёт.

              с означенных событий прошло более 15 лет, с разношерстностью все более-менее ок.


            1. arielf
              08.04.2017 16:18

              Всё зависит от использования. Обычно обнуление очень удобно. Если у вас строка записалась не полностью или вообще не записалась, вы всегда можете глянуть, где она закончится. Многие наоборот считают, что новый интерфейс не интуитивен. Я его не использую, ибо в большинстве UNIX он отсутствует.


              1. qw1
                08.04.2017 16:22

                Если у вас строка записалась не полностью или вообще не записалась, вы всегда можете глянуть, где она закончится
                Понятно, что nul character надо писать. Но только один, зачем занулять весь неиспользуемый остаток буфера. Кстати, если строка записалась не полностью (буфер короче строки), nul character не пишется в strncpy и мы получаем некорректную asciiz-строку.


                1. arielf
                  08.04.2017 16:36

                  Обнуляется не весь буффер, только если длина строки короче, чем нужно было скопировать символов.

                  If, after copying the terminating null character from src, count is not reached, additional null characters are written to dest until the total of count characters have been written.


                  1. qw1
                    08.04.2017 18:56

                    О чём я и писал — «весь неиспользуемый буфер».

                    char dst[4096];
                    strncpy(dst, "test", sizeof(dst));

                    Нужно в dst всего лишь 5 байт записать, а будут записаны все 4096 (4091 записанных байт — оверхед).


                    1. exchg
                      08.04.2017 19:06

                      char * strncpy ( char * destination, const char * source, size_t num );
                      
                      Copies the first num characters of source to destination. (http://www.cplusplus.com/reference/cstring/strncpy/)

                      Вы неправильно используете. Не sizeof(dst), а strlen("test").


                      1. exchg
                        08.04.2017 19:13

                        Ну или


                        strncpy(dst, "some long test", 4);


                        1. exchg
                          08.04.2017 19:23

                          Хотя маны говорят что


                          One valid (and intended) use of strncpy() is to copy a C string to a fixed-length buffer while ensuring both that the buffer is not overflowed and that unused bytes in the target buffer are zeroed out (perhaps to prevent information leaks if the buffer is to be written to media or transmitted to another process via an interprocess communication technique).

                          Так что я скорее неправ.


                    1. arielf
                      09.04.2017 03:10

                      Ну что же вы, честное слово. Спецификации пишут, чтобы их читали. :3

                      char dst[4096];
                      strncpy(dst, "test", strlen("test") + 1); \\ место для '\0'
                      

                      n говорит, сколько символов нужно взять из src. Но вообще, strlcpy() действительно выглядит лучше.


                      1. qw1
                        09.04.2017 13:48

                        strncpy(dst, "test", strlen("test") + 1); \\ место для '\0'
                        Плохой пример. В таком виде никакого смысла в strncpy нет, тут она полностью равноценна strcpy


                  1. qw1
                    08.04.2017 19:04

                    ААА… Тут я начинаю понимать, что strncpy — не замена strcpy_s, как это вы предлагали выше.

                    strncpy — это способ скопировать из некоторого буфера ограниченной длины, в котором должен быть терминирующий nul, но этот факт не гарантируется (например, буфер прочитан из внешнего файла)

                    Например:

                    struct Header
                    {
                        char name[20];
                        char title[30];
                    } header;
                    // ...
                    char localname[21];
                    strncpy(localname, header.name, sizeof(header.name));


                    1. arielf
                      09.04.2017 03:18

                      Я ничего подобного не говорил, и не мог. Грубо говоря, strncpy() может копировать строки у которых отсутствует '\0' в конце. (strcpy() понятное дело не может). Похоже, нужно ещё одно эссе про строковые функции.


                      1. kloppspb
                        09.04.2017 03:27

                        Похоже, нужно ещё одно эссе про строковые функции.
                        Хм… Вон, на ru:SO по тегу «C» регулярно всплывают вопросы уровня «почему не работает код»:

                        if( a == b )
                        /* обе переменные, само собой, char *, и имеется в виду сравнение строк */


                        P.S. А вообще, IMHO, во многих ситуациях strdup() — вполне решение. Но тут уж по конкретике смотреть надо.