Правильно сформированные пары в вызове функции

Это вопрос о нормах стандарта C11, касающихся побочных эффектов, когда аргументы функции оцениваются в выражении.

Я пытаюсь определить макрос в стандартном C, который эмулирует подобный «методу» синтаксис языка ООП, в зачаточном состоянии.
Я разработал решение, основные идеи которого я раскрою здесь, но у меня есть некоторые сомнения относительно его соответствия C11.
Сначала мне нужно сделать экспозицию, а в конце я задам конкретный вопрос, который связан с вычислением выражений, включающих вызовы функций. Извините за длинное сообщение.

Итак, учитывая struct или, ну, struct * объект x, я был бы счастлив, если бы мог сделать вызов "метода" следующим образом:

x->foo_method();  

Типичный способ решения этой проблемы примерно такой:

  • Определите «класс» с помощью объявления struct:

    typedef struct foo_s { void foo_method(struct foo_s * this); } *foo_class;  
    foo_class x   = malloc(sizeof(struct foo_s));  
    x->foo_method = some_function_defined_over_there___;  
    
  • Затем сделайте вызов, повторив объект в параметре ``this'':

    x->foo_method(x);  
    

Можно попытаться определить какой-то макрос "вызова метода":

#define call(X, M) ((X)->M(X))

Однако этот подход плох, так как оценка X может дублировать побочные эффекты (это известная ошибка повторения макропараметра дважды).

[Используя хитрые макросы, я могу обработать случай произвольного количества параметров для метода M, например, используя __VA_ARGS__ и несколько промежуточных макро-хаков.]

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

(void*) my_stack(void* x, char* operation)
{
    static void* stack[100] = { NULL, }; 
    // 'operation' selects "push" or "pop" operations on the stack.
    // ...
    // IF (operation == "push") then 'x' itself is returned again.
}

Итак, теперь я избегаю дублирования побочных эффектов в макросе, написав `X' только один раз:

#define call(X, M) (((foo_class)my_stack((X), "push")) -> M (my_stack(0,"pop")))

Как видите, мое намерение состоит в том, чтобы макрос, похожий на функцию, рассматривался компилятором C как выражение, значение которого является значением, возвращаемым методом M.
Я только один раз написал параметр X внутри макроса. -body, его значение сохранялось в стеке. Поскольку нужно, чтобы это значение имело доступ к члену «метода» самого X, это причина, по которой функция my_stack возвращает значение x: мне нужно немедленно повторно использовать его как часть того же выражения, которое подтолкнуло значение x в стеке.


Хотя эта идея, кажется, легко решает проблему дублирования X в макросе call(X,M), возникает больше проблем.

  • Можно иметь «методы», чьи аргументы также являются объектами, хранящимися в стеке, с помощью того же макроса call().
  • Более того, у нас могли бы быть аргументы в «методе», значения которых получены в результате оценки других «методов».
  • Наконец, другие функции или методы, выступающие в качестве аргументов данного «метода», могут изменять стек, потому что они, вероятно, являются функциями, изменяющими стек с помощью макроса call().

Я хочу, чтобы мой макрос был последовательным во всех этих случаях. Например, предположим, что x1,x2,x3 являются foo_class объектами.
С другой стороны, предположим, что в foo_class у нас есть следующий элемент "метод":

 int (*meth)(foo_class this, int, int);

Наконец, мы могли бы сделать вызов «метода»:

 call(x1, meth, (call (x2, 2, 2), call(x3, 3, 3)) ) ;

[Настоящий синтаксис макроса не обязательно такой, как он показан здесь. Я ожидаю, что основная идея понятна.]

Цель состоит в том, чтобы эмулировать этот вызов функции:

x1->meth(x1, x2->meth(x2,2,2), x3->meth(x3,3,3));  

Проблема здесь в том, что я использую стек для эмуляции следующих дубликатов объектов в вызове: x1->meth(x1,....), x2->meth(x2,...), x3->meth(x3,...).

Например: ((foo_class)(my_stack(x2,"push"))) -> meth (my_stack(0,"pop"), ...).

МОЙ ВОПРОС: Могу ли я всегда быть уверенным, что объединение "push"/"pop" в любом возможном выражении (которое постоянно использует макрос call()) всегда дает ожидаемую пару объектов?

Например, если я «нажимаю» x2, было бы совершенно неправильно, чтобы x3 «выталкивался».

МОЕ ПРЕДЛОЖЕНИЕ: Ответ будет ДА, но после глубокого анализа стандартного документа ISO C11 по теме точек последовательности.

  • Существует точка последовательности между выражением, которое создает "метод" (фактически, "функцию"), который нужно вызвать, и выражениями аргументов, которые должны быть переданы ему. Таким образом, например, x1 сохраняется в стеке до того, как метод meth считается вызванным.
  • Существует точка следования после оценки всех аргументов, переданных в функцию, и перед фактическим вызовом функции.
    Таким образом, например, если новые объекты x4, x5 и т. д. "заталкиваются"/"извлекаются" в стек во время происходит вызов x1->meth(x1...x2...x3), эти объекты x4 и x5 будут появляться и исчезать в стеке после того, как x2, x3 уже ушли из стека.
  • Между аргументами в вызове функции нет точки последовательности.
    Таким образом, следующие выражения могут чередоваться при их вычислении (когда они являются аргументами вызова функции, показанного выше, включая x1,x2,x3):

    my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2) 
    my_stack(x3,"push") -> meth(my_stack(0,"pop"),3,3) 
    

    Могло случиться так, что после того, как объекты x2 и x3 были "задвинуты" в стек, операции "выталкивания" могли происходить непарно: x3 могло "выталкиваться" в строке meth(...,2,2), а x2 могло "выталкиваться" в строке meth(...,3,3). , против желаемого.

    Эта ситуация совершенно маловероятна, и кажется, что в Стандарте C99 нет формального решения.

Однако в C11 у нас есть концепция inderminately sequenced побочных эффектов.
У нас есть следующее:

  • Когда вызывается функция, все ее побочные эффекты разрешаются в неопределенной последовательности относительно любого другого выражения вокруг выражения, которое вызывает вызов функции. [См. параграф (12) здесь: точки последовательности].

  • Так как побочные эффекты при вызове функции meth участвуют в выражении:

     my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2)
    

    должны устранить «полностью до» или «полностью после» побочные эффекты в:

     my_stack(x3,"push") -> meth(my_stack(0,"pop"),3,3) 
    

    Я пришел к выводу, что операции "push" и "pop" хорошо сочетаются.

Возможна ли моя интерпретация стандарта? На всякий случай процитирую:

[C11, 6.5.2.2/10] Существует точка последовательности после оценки указателя функции и фактических аргументов, но перед фактическим вызовом. Каждое вычисление в вызывающей функции (включая вызовы функций), которое не имеет определенной последовательности до или после выполнения тела вызываемой функции, имеет неопределенную последовательность относительно выполнения вызываемой функции.94)
[Сноска 94]: Другими словами, выполнение функций не "перемежается" друг с другом.

То есть, хотя порядок оценки аргументов в вызове функции нельзя предсказать, я думаю, что можно, так или иначе, быть уверенным, что правил «последовательности», установленных в ISO C11, достаточно для того, чтобы гарантировать, что «толчок» и « pop" хорошо работают в этом случае.

Таким образом, синтаксис, подобный «методу», может использоваться в C, чтобы эмулировать рудиментарную, но последовательную возможность ООП «методы как члены объектов».


person pablo1977    schedule 04.08.2016    source источник
comment
Во-первых, не typedef указатели! Это просто приводит к путанице. Второе: не увлекайтесь макросами. Вы можете использовать ООП в C, но есть линия, использующая другой язык, такой как C++, является лучшим выбором. Предпосылка должна быть удобочитаемостью/обслуживаемостью (поэтому мы могли бы использовать ООП на самом деле). Я попал в одну и ту же ловушку раз или два.   -  person too honest for this site    schedule 04.08.2016
comment
О, и ваш вопрос не очень подходит для переполнения стека, так как это скорее обсуждение, чем конкретный вопрос (по крайней мере, он привлечет мнения/ответы. Это не дискуссионный форум, а сайт вопросов и ответов. См. Как спросить.   -  person too honest for this site    schedule 04.08.2016
comment
@Olaf: Я знаю советы против использования ООП в C. Но иногда может быть необходимо иметь хотя бы некоторые элементарные ООП в C, не изучая много С++, если будет использоваться только минимальное количество аспектов ООП. Конечно, если вам нужны все более и более изощренные методы ООП, использование C++ является обязательным.   -  person pablo1977    schedule 04.08.2016
comment
синтаксис, подобный методу, может использоваться в C. Вызов функций через указатели на функции в точности выглядит как методы, но другой поддержки нет. ООП не означает аналог С++. Существует больше (и очень разных) способов реализации ООП, чем C++. Ваш подход вызовет экспоненциальные проблемы с наследованием и т.д.   -  person too honest for this site    schedule 04.08.2016
comment
@Olaf: Мой вопрос касается интерпретации стандарта. Вопросы о методах существуют только для того, чтобы быть честными с вами в отношении контекста.   -  person pablo1977    schedule 04.08.2016
comment
Я ни в коем случае не писал, что вы не должны использовать ООП в C!   -  person too honest for this site    schedule 04.08.2016
comment
Затем спросите о стандарте. Сократите свой вопрос до того, что вы особенно задаете. Нет нужды рассказывать, что вы планируете на ближайшие десять лет. См. Как спросить!   -  person too honest for this site    schedule 04.08.2016
comment
@Olaf: Хорошо, я попробую спросить совета. (Честно говоря, я не знаю, как сделать пост более лаконичным... :( )   -  person pablo1977    schedule 04.08.2016
comment
Вы пытаетесь использовать макросы для удобства, чтобы создать свой собственный ООП «язык» на C, или нет другого способа сделать то, что вы хотите? Если макросы создают больше проблем, чем решают: отбросьте их, хотя это может быть болезненно, если вы откажетесь от тяжелой работы.   -  person Weather Vane    schedule 04.08.2016
comment
@WeatherVane: для студента C быстрее осваивается, чем C++. С другой стороны, неплохо было бы немного базового ООП, с небольшим количеством синтаксического сахара и без необходимости изучения всех тонкостей C++. Но если цена завышена, то я с вами согласен. Лучше отказаться от этой идеи.   -  person pablo1977    schedule 04.08.2016
comment
В C нет синтаксического сахара для ООП. Он просто не поддерживает ООП! Все, что вы можете сделать, это возиться с макросами. Не знаю, зачем они вам. Но, как вы сказали мне, вы спрашиваете о конкретной конструкции, почему бы вам просто не бросить все описание доморощенного ООП и не задать конкретный вопрос, как того требуют правила сайта?   -  person too honest for this site    schedule 04.08.2016
comment
@ pablo1977 pablo1977 Я не предлагал вам отказаться от использования C для простого приложения ООП, а только решал проблему с макросами - если они вызывают проблемы. Но одна из причин, по которой С++ взлетел, заключалась в том, чтобы включить это.   -  person Weather Vane    schedule 04.08.2016
comment
@WeatherVane: Да, я понимаю вашу точку зрения. Спасибо за ваше мнение.   -  person pablo1977    schedule 04.08.2016


Ответы (1)


Нет, я не думаю, что вы можете гарантировать, что это сделает то, что вы хотите. Давайте разложим ваше выражение

my_stack(x2,"push") -> meth(my_stack(0,"pop"),2,2)
<<<<<< A >>>>>>>>>>         <<<<<<< B >>>>>>
<<<<<<<<<<<<< C >>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<< D >>>>>>>>>>>>>>>>>>>>>>>>

Вычисления B и C полностью независимы и должны выполняться перед вызовом функции D. Аргументы функции и указатель функции для этого не сильно отличаются.

Поскольку A и B являются вызовами функций, они на самом деле упорядочены, но это неопределенно, поэтому вы не знаете, какой из них будет первым, а какой вторым.

Я думаю, вам было бы намного лучше, если бы вы сделали свою call встроенной функцией. Если вам действительно нужны версии call для разных типов, вы можете выбрать функцию с выражением _Generic. Но, как кто-то уже сказал в комментариях, вы действительно находитесь в пределах того, что вы должны разумно делать в C.

person Jens Gustedt    schedule 04.08.2016
comment
Спасибо за ваш ответ. Однако у меня есть сомнения, поскольку стандарт утверждает, что после обозначения функции есть точка последовательности. [6.5.2.2/10]: There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call. Означает ли это, что вычисления назначения функции и некоторого аргумента могут выполняться в любом порядке? Например, сначала оценивается аргумент, а затем указатель функции? - person pablo1977; 04.08.2016
comment
да, как вы можете видеть из текста, который вы цитируете, указатель функции и аргументы образуют одну группу неупорядоченных выражений. В вашем случае они затем неопределенно упорядочены, потому что сами содержат вызовы функций. Но порядок не установлен. - person Jens Gustedt; 04.08.2016
comment
@ pablo1977, я повторяю замечание Йенса, потому что вы дважды сделали одно и то же ложное утверждение о положениях стандарта: между оценкой указателя функции и оценкой аргументов функции нет точки последовательности. Стандарт говорит, скорее, что есть точка следования после того, как все были оценены. Их можно оценивать в любом относительном порядке. - person John Bollinger; 04.08.2016
comment
@JohnBollinger: Спасибо вам обоим за помощь. Теперь я вижу свою неправильную интерпретацию стандарта. - person pablo1977; 04.08.2016