Як домогтися перевантаження функцій у С?


240

Чи є спосіб досягти перевантаження функцій у С? Я дивлюся на прості функції, як, наприклад, перевантажуватися

foo (int a)  
foo (char b)  
foo (float c , int d)

Я думаю, що прямого прямого шляху немає; Я шукаю обхідні шляхи, якщо такі існують.


6
Чому б ти хотів це зробити? C не має поліморфних здібностей. Тож foo (випадковий тип) неможливо. Просто зробіть справжні функції funo_i, foo_ch, foo_d тощо
jmucchiello

4
Ви можете піти злим шляхом, використовуючи недійсні покажчики та введіть ідентифікатори.
алк

11
Я відчуваю, що слід звернути увагу на той факт, що відповідь на це питання змінився з моменту його спочатку , з новим стандартом C.
Левшенко

Відповіді:


127

Є кілька можливостей:

  1. функції стилю printf (введіть як аргумент)
  2. функції стилю opengl (введіть назву функції)
  3. c підмножина c ++ (якщо ви можете використовувати компілятор c ++)

1
Ви можете пояснити або надати посилання для функцій стилю opengl?
FL4SOF

1
@Lazer: Ось одна проста функція, схожа на printf.
Олексій Фрунзе

12
Ні. Printf не є функцією перевантаження. він використовує vararg !!! І C не підтримує перевантаження функцій.
hqt

52
@hqt Відповідь ніколи не згадує про перевантаження слова.
kyrias

1
@kyrias Якщо відповідь не про перевантаження, це неправильне запитання
Michael Mrozek

233

Так!

За час, коли це питання було задано, стандартний C (без розширень) фактично отримав підтримку перевантаження функцій (не операторів), завдяки додаванню _Genericключового слова в C11. (підтримується в GCC з версії 4.9)

(Перевантаження не є справді «вбудованим» у спосіб, показаний у питанні, але реально реалізувати щось подібне.

_Genericє оператором часу компіляції в тій же сім'ї, що sizeofі _Alignof. Це описано в стандартному розділі 6.5.1.1. Він приймає два основні параметри: вираз (який не буде оцінюватися під час виконання) та список асоціацій типу / вираження, який трохи схожий на switchблок. _Genericотримує загальний тип виразу, а потім "перемикається" на нього, щоб вибрати вираз кінцевого результату у списку для його типу:

_Generic(1, float: 2.0,
            char *: "2",
            int: 2,
            default: get_two_object());

Вищеописаний вираз 2визначає - тип керуючого виразу є int, тому він вибирає вираз, пов'язаний із intзначенням. Нічого з цього не залишається під час виконання. ( defaultПункт необов’язковий: якщо залишити його і тип не збігається, це призведе до помилки компіляції.)

Спосіб, який корисний для перевантаження функцій, полягає в тому, що він може бути вставлений препроцесором C і вибрати вираження результату на основі типу аргументів, переданих керуючому макросу. Отже (приклад зі стандарту С):

#define cbrt(X) _Generic((X),                \
                         long double: cbrtl, \
                         default: cbrt,      \
                         float: cbrtf        \
                         )(X)

Цей макрос реалізує перевантажену cbrtоперацію, передаючи тип аргументу макросу, вибираючи відповідну функцію реалізації та передаючи початковий аргумент макросу цій функції.

Отже, щоб реалізувати ваш оригінальний приклад, ми могли б зробити це:

foo_int (int a)  
foo_char (char b)  
foo_float_int (float c , int d)

#define foo(_1, ...) _Generic((_1),                                  \
                              int: foo_int,                          \
                              char: foo_char,                        \
                              float: _Generic((FIRST(__VA_ARGS__,)), \
                                     int: foo_float_int))(_1, __VA_ARGS__)
#define FIRST(A, ...) A

У цьому випадку ми могли використати default:об'єднання для третього випадку, але це не демонструє, як поширити принцип на кілька аргументів. Кінцевим результатом є те, що ви можете використовувати foo(...)у своєму коді, не переживаючи (багато [1]) про тип його аргументів.


Для більш складних ситуацій, наприклад функцій, що перевантажують більшу кількість аргументів або різні числа, ви можете використовувати макроси утиліти для автоматичного генерування статичних структур диспетчеризації:

void print_ii(int a, int b) { printf("int, int\n"); }
void print_di(double a, int b) { printf("double, int\n"); }
void print_iii(int a, int b, int c) { printf("int, int, int\n"); }
void print_default(void) { printf("unknown arguments\n"); }

#define print(...) OVERLOAD(print, (__VA_ARGS__), \
    (print_ii, (int, int)), \
    (print_di, (double, int)), \
    (print_iii, (int, int, int)) \
)

#define OVERLOAD_ARG_TYPES (int, double)
#define OVERLOAD_FUNCTIONS (print)
#include "activate-overloads.h"

int main(void) {
    print(44, 47);   // prints "int, int"
    print(4.4, 47);  // prints "double, int"
    print(1, 2, 3);  // prints "int, int, int"
    print("");       // prints "unknown arguments"
}

( реалізація тут ) Отже, доклавши певних зусиль, ви можете зменшити кількість котлоавтоматики, щоб виглядати майже як мова з рідною підтримкою перевантаження.

На відміну від цього вже можна було перевантажити кількість аргументів (не тип) у C99.


[1] зауважте, що спосіб C, який оцінює типи, може вас привернути. Це вибереться, foo_intякщо ви спробуєте передати його, наприклад, літеральному символу, і вам потрібно трохи заплутатися, якщо ви хочете, щоб ваші перевантаження підтримували рядкові літерали. Досі загальний досить крутий, хоча.


На основі вашого прикладу виглядає, що єдине, що перевантажується, це функція, як макроси. Дозвольте мені зрозуміти, чи правильно я розумію: Якщо ви хочете перевантажити функції, ви просто використовували б препроцесор для переадресації виклику функції на основі типів даних, переданих, правда?
Нік

На жаль, кожного разу, коли C11 починає вловлюватись, я припускаю, що MISRA не буде використовувати цю функцію з тих же причин, які забороняють переліки аргументів змінних аргументів. Я намагаюся дотримуватися MISRA досить близько в моєму світі.
Нік

9
@ Nick, це все перевантаження. Він просто обробляється неявно іншими мовами (наприклад, ви не можете реально отримати "покажчик на перевантажену функцію" будь-якою мовою, оскільки перевантаження має на увазі декілька тіл). Зауважте, що це не може зробити препроцесор самостійно, для цього потрібна певна відправка типу; препроцесор просто змінює, як він виглядає.
Левшенко

1
Оскільки хтось досить добре знайомий із C99 і хоче навчитися робити це, це здається надто складним, навіть для C.
Тайлер Кромптон

5
@TylerCrompton Оцінюється під час компіляції.
JAB

75

Як вже було сказано, перевантаження в тому сенсі, який ви маєте на увазі, не підтримується C. Загальною ідіомою для вирішення проблеми є змушення функції приймати теговий союз . Це реалізується structпараметром, де structсам по собі складається з якогось типу індикатора типу, наприклад enum, а та unionрізних типів значень. Приклад:

#include <stdio.h>

typedef enum {
    T_INT,
    T_FLOAT,
    T_CHAR,
} my_type;

typedef struct {
    my_type type;
    union {
        int a; 
        float b; 
        char c;
    } my_union;
} my_struct;

void set_overload (my_struct *whatever) 
{
    switch (whatever->type) 
    {
        case T_INT:
            whatever->my_union.a = 1;
            break;
        case T_FLOAT:
            whatever->my_union.b = 2.0;
            break;
        case T_CHAR:
            whatever->my_union.c = '3';
    }
}

void printf_overload (my_struct *whatever) {
    switch (whatever->type) 
    {
        case T_INT:
            printf("%d\n", whatever->my_union.a);
            break;
        case T_FLOAT:
            printf("%f\n", whatever->my_union.b);
            break;
        case T_CHAR:
            printf("%c\n", whatever->my_union.c);
            break;
    }

}

int main (int argc, char* argv[])
{
    my_struct s;

    s.type=T_INT;
    set_overload(&s);
    printf_overload(&s);

    s.type=T_FLOAT;
    set_overload(&s);
    printf_overload(&s);

    s.type=T_CHAR;
    set_overload(&s);
    printf_overload(&s); 
}

22
Чому б вам не тільки зробити все whateverS на окремі функції ( set_int, set_floatі т.д.). Потім "позначення типу" стає "додавання імені типу до імені функції". Версія у цій відповіді передбачає більше набору тексту, більше витрат на виконання, більше шансів на помилки, які не будуть виявлені під час компіляції ... Я не бачу жодної переваги робити такі дії! 16 оновлень ?!
Бен

20
Бен, ця відповідь сприйнята, оскільки вона відповідає на питання, а не просто каже "не роби цього". Ви вірні, що в C більш ідіоматично використовувати окремі функції, але якщо хочеться поліморфізму в С, це хороший спосіб це зробити. Далі ця відповідь показує, як ви реалізували б поліморфізм під час виконання у компіляторі або VM: позначте значення типом, а потім виправте його на основі цього. Таким чином, це відмінна відповідь на початкове запитання.
Нілс фон Барт

20

Ось найяскравіший і стислий приклад, який я знайшов, демонструючи перевантаження функцій у C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int addi(int a, int b) {
    return a + b;
}

char *adds(char *a, char *b) {
    char *res = malloc(strlen(a) + strlen(b) + 1);
    strcpy(res, a);
    strcat(res, b);
    return res;
}

#define add(a, b) _Generic(a, int: addi, char*: adds)(a, b)

int main(void) {
    int a = 1, b = 2;
    printf("%d\n", add(a, b)); // 3

    char *c = "hello ", *d = "world";
    printf("%s\n", add(c, d)); // hello world

    return 0;
}

https://gist.github.com/barosl/e0af4a92b2b8cabd05a7


1
Я думаю, що це дух духу stackoverflow.com/a/25026358/1240268 (але з меншим поясненням).
Енді Хайден

1
Я, безумовно, віддаю перевагу одному єдиному безперервному блоку повного та розгорнутого коду перед нарізкою та нарізкою, який є # 1240268. Кожному своє.
Джей Тейлор

1
Я вважаю за краще відповіді, які пояснюють, що вони роблять і чому вони працюють. Це не робить жодного. "Найкраще, що я бачив поки:" - це не експозиція.
підкреслити_

19

Якщо ваш компілятор - gcc, і ви не проти робити оновлення вручну кожного разу, коли ви додаєте нову перевантаження, ви можете зробити макро-магію і отримати результат, який ви хочете, що стосується абонентів, це не так приємно писати ... але це можливо

подивіться на __builtin_types_compatible_p, а потім використовуйте його для визначення макросу, який робить щось на кшталт

#define foo(a) \
((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

але так неприємно, просто не варто

EDIT: C1X отримуватиме підтримку для загальних виразів типу, які вони виглядають так:

#define cbrt(X) _Generic((X), long double: cbrtl, \
                              default: cbrt, \
                              float: cbrtf)(X)

13

Так, начебто.

Ось вам на приклад:

void printA(int a){
printf("Hello world from printA : %d\n",a);
}

void printB(const char *buff){
printf("Hello world from printB : %s\n",buff);
}

#define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 
#define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N
#define _Num_ARGS_(...) __VA_ARG_N(__VA_ARGS__) 
#define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) 
#define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args)>t)
#define CHECK_ARGS_MIN_LIMIT(t) if(NUM_ARGS(args) 
#define print(x , args ...) \
CHECK_ARGS_MIN_LIMIT(1) printf("error");fflush(stdout); \
CHECK_ARGS_MAX_LIMIT(4) printf("error");fflush(stdout); \
({ \
if (__builtin_types_compatible_p (typeof (x), int)) \
printA(x, ##args); \
else \
printB (x,##args); \
})

int main(int argc, char** argv) {
    int a=0;
    print(a);
    print("hello");
    return (EXIT_SUCCESS);
}

Він виведе 0 і привіт .. з printA та printB.


2
int main (int argc, char ** argv) {int a = 0; друк (а); друк ("привіт"); повернення (EXIT_SUCCESS); } виведе 0 і привіт .. з printA і printB ...
Капітан Барбосса

1
__builtin_types_compatible_p, чи не конкретний компілятор GCC?
Sogartar

11

Наступний підхід схожий на a2800276 , але з доданими деякими макро магіями C99:

// we need `size_t`
#include <stddef.h>

// argument types to accept
enum sum_arg_types { SUM_LONG, SUM_ULONG, SUM_DOUBLE };

// a structure to hold an argument
struct sum_arg
{
    enum sum_arg_types type;
    union
    {
        long as_long;
        unsigned long as_ulong;
        double as_double;
    } value;
};

// determine an array's size
#define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY)))

// this is how our function will be called
#define sum(...) _sum(count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__))

// create an array of `struct sum_arg`
#define sum_args(...) ((struct sum_arg []){ __VA_ARGS__ })

// create initializers for the arguments
#define sum_long(VALUE) { SUM_LONG, { .as_long = (VALUE) } }
#define sum_ulong(VALUE) { SUM_ULONG, { .as_ulong = (VALUE) } }
#define sum_double(VALUE) { SUM_DOUBLE, { .as_double = (VALUE) } }

// our polymorphic function
long double _sum(size_t count, struct sum_arg * args)
{
    long double value = 0;

    for(size_t i = 0; i < count; ++i)
    {
        switch(args[i].type)
        {
            case SUM_LONG:
            value += args[i].value.as_long;
            break;

            case SUM_ULONG:
            value += args[i].value.as_ulong;
            break;

            case SUM_DOUBLE:
            value += args[i].value.as_double;
            break;
        }
    }

    return value;
}

// let's see if it works

#include <stdio.h>

int main()
{
    unsigned long foo = -1;
    long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10));
    printf("%Le\n", value);
    return 0;
}

11

Це може не допомогти зовсім, але якщо ви використовуєте кланг, ви можете використовувати атрибут, що можна завантажити - Це працює навіть при компіляції як C

http://clang.llvm.org/docs/AttributeReference.html#overloadable

Заголовок

extern void DecodeImageNow(CGImageRef image, CGContextRef usingContext) __attribute__((overloadable));
extern void DecodeImageNow(CGImageRef image) __attribute__((overloadable));

Впровадження

void __attribute__((overloadable)) DecodeImageNow(CGImageRef image, CGContextRef usingContext { ... }
void __attribute__((overloadable)) DecodeImageNow(CGImageRef image) { ... }

10

У сенсі ви маєте на увазі - ні, ви не можете.

Ви можете оголосити va_argфункцію типу

void my_func(char* format, ...);

, але вам потрібно буде передати якусь інформацію про кількість змінних та їх типи в першому аргументі, як printf()і в.


6

Зазвичай бородавка для позначення типу додається або додається до імені. Ви можете піти з макросів - це деякі випадки, але це швидше залежить від того, що ви намагаєтеся зробити. У С немає поліморфізму, лише примус.

Прості загальні операції можна виконати за допомогою макросів:

#define max(x,y) ((x)>(y)?(x):(y))

Якщо ваш компілятор підтримує typeof , в макрос можна помістити більш складні операції. Потім ви можете мати символ foo (x) для підтримки однієї і тієї ж операції різних типів, але ви не можете змінювати поведінку між різними перевантаженнями. Якщо вам потрібні фактичні функції, а не макроси, ви, можливо, зможете вставити тип до імені та скористатися другою вставкою для доступу до нього (я не намагався).


Ви можете пояснити трохи більше на основі макроконтролю
FL4SOF

4

Відповідь Левшенка справді класна - виключно: fooприклад не компілюється з GCC, який не вдається foo(7), натрапляючи на FIRSTмакрос і фактичний виклик функції ( (_1, __VA_ARGS__)залишаючись із надлишковою комою. Крім того, ми маємо біду, якщо хочемо забезпечити додаткові перевантаження , таких як foo(double).

Тому я вирішив детальніше відповісти, в тому числі, щоб дозволити порожнечу перевантаження ( foo(void)- це спричинило чимало клопоту ...).

Зараз ідея: Визначте більше одного загального у різних макросах і нехай виберіть правильний відповідно до кількості аргументів!

Кількість аргументів досить проста, виходячи з цієї відповіді :

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__)

#define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__)
#define CONCAT(X, Y) CONCAT_(X, Y)
#define CONCAT_(X, Y) X ## Y

Це добре, ми вирішуємо будь-який SELECT_1або SELECT_2(або більше аргументів, якщо ви хочете / потрібні), тому нам просто потрібні відповідні визначення:

#define SELECT_0() foo_void
#define SELECT_1(_1) _Generic ((_1),    \
        int: foo_int,                   \
        char: foo_char,                 \
        double: foo_double              \
)
#define SELECT_2(_1, _2) _Generic((_1), \
        double: _Generic((_2),          \
                int: foo_double_int     \
        )                               \
)

Гаразд, я вже додав порожнечу перевантаження - однак ця фактично не охоплена стандартом C, що не дозволяє порожні різні аргументи, тобто ми покладаємось на розширення компілятора !

Спочатку порожній макро дзвінок ( foo()) все ще виробляє маркер, але порожній. Отже макрос підрахунку фактично повертає 1 замість 0 навіть при порожньому виклику макросу. Ми можемо «легко» усунути цю проблему, якщо поставити кому після __VA_ARGS__ умовно , залежно від того, чи список порожній чи ні:

#define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

Це виглядало легко, але COMMAмакрос досить важкий; на щастя, тема вже висвітлена в блозі Йенса Гуведта (спасибі, Єнс). Основна хитрість полягає в тому, що макроси функцій не розширюються, якщо не супроводжуються дужками, для подальшого пояснення ознайомтеся з блогом Йенса ... Нам просто потрібно трохи змінити макроси під наші потреби (я буду використовувати короткі назви і менше аргументів для стислості).

#define ARGN(...) ARGN_(__VA_ARGS__)
#define ARGN_(_0, _1, _2, _3, N, ...) N
#define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0)

#define SET_COMMA(...) ,

#define COMMA(...) SELECT_COMMA             \
(                                           \
        HAS_COMMA(__VA_ARGS__),             \
        HAS_COMMA(__VA_ARGS__ ()),          \
        HAS_COMMA(SET_COMMA __VA_ARGS__),   \
        HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \
)

#define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3)
#define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3

#define COMMA_0000 ,
#define COMMA_0001
#define COMMA_0010 ,
// ... (all others with comma)
#define COMMA_1111 ,

А зараз у нас все добре ...

Повний код в одному блоці:

/*
 * demo.c
 *
 *  Created on: 2017-09-14
 *      Author: sboehler
 */

#include <stdio.h>

void foo_void(void)
{
    puts("void");
}
void foo_int(int c)
{
    printf("int: %d\n", c);
}
void foo_char(char c)
{
    printf("char: %c\n", c);
}
void foo_double(double c)
{
    printf("double: %.2f\n", c);
}
void foo_double_int(double c, int d)
{
    printf("double: %.2f, int: %d\n", c, d);
}

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__)

#define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__)
#define CONCAT(X, Y) CONCAT_(X, Y)
#define CONCAT_(X, Y) X ## Y

#define SELECT_0() foo_void
#define SELECT_1(_1) _Generic ((_1), \
        int: foo_int,                \
        char: foo_char,              \
        double: foo_double           \
)
#define SELECT_2(_1, _2) _Generic((_1), \
        double: _Generic((_2),          \
                int: foo_double_int     \
        )                               \
)

#define ARGN(...) ARGN_(__VA_ARGS__)
#define ARGN_(_0, _1, _2, N, ...) N

#define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0)
#define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0)

#define SET_COMMA(...) ,

#define COMMA(...) SELECT_COMMA             \
(                                           \
        HAS_COMMA(__VA_ARGS__),             \
        HAS_COMMA(__VA_ARGS__ ()),          \
        HAS_COMMA(SET_COMMA __VA_ARGS__),   \
        HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \
)

#define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3)
#define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3

#define COMMA_0000 ,
#define COMMA_0001
#define COMMA_0010 ,
#define COMMA_0011 ,
#define COMMA_0100 ,
#define COMMA_0101 ,
#define COMMA_0110 ,
#define COMMA_0111 ,
#define COMMA_1000 ,
#define COMMA_1001 ,
#define COMMA_1010 ,
#define COMMA_1011 ,
#define COMMA_1100 ,
#define COMMA_1101 ,
#define COMMA_1110 ,
#define COMMA_1111 ,

int main(int argc, char** argv)
{
    foo();
    foo(7);
    foo(10.12);
    foo(12.10, 7);
    foo((char)'s');

    return 0;
}

1

Ви не можете просто використовувати C ++ і не використовувати всі інші функції C ++, крім цієї?

Якщо все-таки немає просто строгого C, то я б рекомендував натомість різні варіанти .


3
Ні, якщо компілятор C ++ недоступний для ОС, для якої він кодує.
Брайан

2
не тільки це, але він може захотіти C ABI, який не має в ньому імені.
Spudd86

-3

Спробуйте оголосити ці функції так, extern "C++"ніби ваш компілятор підтримує це, http://msdn.microsoft.com/en-us/library/s6y4zxec(VS.80).aspx


3
Це може змінити керування іменами, щоб надати їм унікальні імена (напевно, ні), але це раптом не дасть правила вирішення C перевантаження.
Ben Voigt

-4

Я сподіваюся, що наведений нижче код допоможе вам зрозуміти перевантаження функцій

#include <stdio.h>
#include<stdarg.h>

int fun(int a, ...);
int main(int argc, char *argv[]){
   fun(1,10);
   fun(2,"cquestionbank");
   return 0;
}
int fun(int a, ...){
  va_list vl;
  va_start(vl,a);

  if(a==1)
      printf("%d",va_arg(vl,int));
   else
      printf("\n%s",va_arg(vl,char *));
}

2
Відповідь повинна пояснити, що це робить і чому це працює. Якщо це не так, то як він може допомогти комусь зрозуміти що-небудь?
підкреслюйте_d

Тут немає перевантаження.
мельпомена

va_end ніколи не дзвонили
користувач2262111
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.