Перегрузка функций. Директива extern C

Практический курс по C/C++: https://stepik.org/course/193691

Начиная с этого занятия, рассмотрим несколько значимых новшеств определения функций, которые появились в С++. Я не буду повторяться, что такое функции, как они объявляются и вызываются. Об этом мы с вами уже подробно говорили на курсе по языку Си. На этом занятии речь пойдет о перегрузке функций. Что это такое?

Давайте представим, что нам в программе нужна функция, которая бы вычисляла модуль чисел в диапазоне [-10; 10]. При этом, числа могут быть как целыми, так и вещественными (дробными). Если бы мы оставались в рамках языка Си, то было бы целесообразно объявить две функции: одна для целочисленных значений, а вторая – для вещественных. Например, так:

int modul_int(int x)
{
    cout << "modul(int)" << endl;
    if(x >= -10 && x <= 10)
        return (x > 0) ? x : -x;
    return x;
}
 
double modul_double(double x)
{
    cout << "modul(double)" << endl;
    if(x >= -10 && x <= 10)
        return (x > 0) ? x : -x;
    return x;
}

То есть, имена у этих функций должны были бы различаться. Но С++ позволяет объявлять функции с одинаковыми именами, но разными типами входных параметров. И наш пример можно записать в следующем виде:

int modul(int x) ...
double modul(double x) ...

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

Хорошо, но что нам это дает? Давайте вспомним, что на чистом Си имеются функции вида:

  • long labs(long); - для вычисления модуля целых чисел;
  • double fabs(double); - для вычисления модуля вещественных чисел.

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

  • int abs(int);
  • double abs(double);

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

int main()
{
    double res_1 = modul(-0.5);  // modul(double)
    int res_2 = modul(-0.5);        // modul(double)
    double res_3 = modul(-5);    // modul(int)
    int res_4 = modul(-5);           // modul(int)
 
    return 0;
}

В консоли увидим:

modul(double)
modul(double)
modul(int)
modul(int)

То есть, в первых двух случаях компилятор подставил функцию с параметром double, а во вторых двух – функцию с параметром int. При этом тип переменной, которой присваивается результат, никак не влияет на выбор той или иной функции. И это логично. Главное, вызвать функцию с наиболее подходящим набором параметров, а куда мы присвоим результат – это уже задача программиста, использующего эту перегруженную функцию. Таким образом, компилятор выбирает тот или иной вариант объявления, опираясь на типы передаваемых аргументов при вызове функции. Причем, типы можно указывать меньших размерностей, например, в параметр double передавать число типа float, или в параметр int число типа short. А вот при передаче больших размерностей компилятор выдаст сообщение об ошибке, т.к. в этом случае возможна потеря данных:

int main()
{
    double res_1 = modul(-0.5f);  // ok
    int res_2 = modul((long double)(-0.5));       // ошибка
    double res_3 = modul(-5L); // ошибка
    int res_4 = modul((short)-5);   // ok
 
    return 0;
}

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

Особенности перегрузки имен функций. Директива extern "C"

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

Надо сказать, что механизм перегрузки функций в С++ повлек за собой некоторые особенности при сборке проектов в исполняемый файл. Ранее, когда создавался язык Си, то функции имели уникальные имена. Соответственно, компоновщик, встречая имя функции в объектном файле, подставлял вместо него соответствующий код вызова. Например, имя функция modul_int() в Си внутри объектного файла имела метку modul_int. Все просто. Но с перегрузкой имен функций все изменилось. Теперь мало просто указать ее имя, нужно дополнительно закодировать в объектном файле набор ее параметров, так как именно эта связка:

имя функции + параметры

дает уникальную метку. Проблема в том, что разные компиляторы языка С++ по разному выполняют это кодирование. Например, компилятор g++ для функции:

int modul(int x);

сформирует метку вида:

_Z5moduli

Здесь _Z – это специальный префикс, означающий начало метки функции; число 5 – количество символов в имени функции; i – внутреннее обозначение стандартного типа int.

Повторюсь, другие компиляторы языка С++ и даже разные версии одного и того же компилятора, могут по разному строить эти метки. К чему это в итоге приводит? Все верно. Если у нас есть только объектные файлы какого-либо проекта, то не всегда нам удастся его собрать и получить конечный результат, например, в виде исполняемого файла. Мало того, если объектные файлы были созданы компилятором языка Си, в котором не подразумевается перегрузка функций, то кодирование меток точно будет неверным для компилятора С++.

Но вот эта последняя проблема все же решаема. Если мы пишем программу на языке Си и предполагаем использовать ее, в том числе, и на С++, то объявление функций следует поместить в тело директивы extern "C" следующим образом:

extern "C" {
 
void show_msg(const char* msg)
{
    puts("-----------------------");
    puts(msg);
    puts("-----------------------");
}
 
}

Или, если в блоке одна функция, то можно просто указать:

extern "C"
void show_msg(const char* msg)
{
    puts("-----------------------");
    puts(msg);
    puts("-----------------------");
}

Разумеется, внутри директивы extern "C" не допускается определять перегруженные функции, т.к. метки будут формироваться и восприниматься компоновщиком в стиле языка Си.

В результате, мы имеем функцию show_msg(), которая в объектном файле будет представлена своим именем show_msg и успешно обрабатываться различными компиляторами языка С++. Однако директива extern "C" доступна в языке С++ и не существует в языке Си. Поэтому для полной универсальности кода, следует добавить следующие директивы условной компиляции:

#ifdef __cplusplus
extern "C" {
#endif
void show_msg(const char* msg)
{
    puts("-----------------------");
    puts(msg);
    puts("-----------------------");
}
 
#ifdef __cplusplus
}
#endif

Здесь макросимвол __cplusplus всегда существует для компилятора С++ и изначально отсутствует в компиляторе языка Си. Поэтому язык Си «не увидит» директиву extern "C", а С++ успешно ее обработает. В итоге, получаем универсальное объявление функции show_msg() для компиляторов обоих языков.

Конечно, это следует делать только в том случае, если мы, например, пишем библиотеку общую для Си и С++ или модуль, который предполагается использовать в этих языках. Тогда нужно добавлять директиву extern "C". Если же программа пишется исключительно для Си или С++, то функции объявляются обычным образом.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме