Сводка вопроса
Существует ли безопасная по типу замена аргументов для функций, которые принимают int
, но которые интерпретируют значение как набор побитовых значений, которые представляют взаимоисключающие операции, но препятствуют вызывающимот использования магических чисел для представления этих операций?Это необходимо для работы с GCC 4.8.5 без обновления.
Подробный вопрос
Я унаследовал старую функцию C ++ в стиле C, которая принимает аргумент int
, указывающий на выполнение некоторых операций.,Эти операции в настоящее время передаются функции как 0x1 | 0x2
и распространяются по всей базе кода.Это, на мой взгляд, довольно неуправляемая ситуация: требуется, чтобы вызывающие абоненты использовали магические числа, и поэтому вынуждает разработчиков читать всю реализацию функции просто для понимания запрашиваемых операций.
Таким образом, япопытайтесь изменить интерфейс, чтобы использовать имена, которые четко указывают, что запрашивается от функции для выполнения, сохраняя при этом большую часть существующего интерфейса функции, кроме изменения типа конкретного аргумента.В краткосрочной перспективе я не могу реорганизовать функцию, чтобы использовать надлежащий полиморфный подход, в котором операции представлены в виде отдельных классов, поскольку использование этих магических чисел стало слишком распространенным, чем в одной операции рефакторинга.Я хочу потребовать, чтобы любые дополнительные операции, добавленные к функции, затем требовали изменения какого-либо центрального типа, чтобы четко указывать все допустимые именованные комбинации операций, которые должна выполнять функция, вместо того, чтобы позволить нетерпеливым разработчикам роскошь взломать еще одно значение, например0x6
и изменение внутренних функций функции для проверки этого значения с использованием постоянно растущей неосуществимой условной логики.В моём макете ниже я определил тип SomeEnum
и показал, как он все еще можно взломать (см. SCENARIO_4
).Я хочу заручиться поддержкой хакера.
Лучшее, что я смог придумать, - это класс SomeEnumImposter
ниже.Это неидеально с точки зрения вызывающей стороны, так как неудобно вводить выражения вида:
SomeEnumImposterUsingFunction(SomeEnumImposter().C().D());
В идеале я мог бы сделать это:
SomeEnumImposterUsingFunction(EIx(C, D));
где EIx
будет неким типом конструкции, которая расширится до этого громоздкого выражения выше.Но для этого мне пришлось бы прибегнуть к использованию переменных макросов, специфичных для GCC, или чтобы избежать этого, мне пришлось бы вручную расширять макросы вида:
#define EI1(x1) SomeEnumImposter().x1()
#define EI2(x1, x2) SomeEnumImposter().x1().x2()
#define EI3(x1, x2, x3) SomeEnumImposter().x1().x2().x3()
#define EI4(x1, x2, x3, x4) SomeEnumImposter().x1().x2().x3().x4()
// etc.
Честно говоря,этот класс SomeEnumImposter
содержит достаточно много кода для защиты от нетерпеливых разработчиков.Есть ли более простой способ, который работает с этим конкретным компилятором (обновления компилятора не разрешены; см. Ниже, выгрузка версии gcc).
Обновление # 1
Добавлена CLASS_WITH_BOOL_DATAMEMBERS
как попыткапри использовании struct Options
, который был упомянут в ответе на https://stackoverflow.com/a/52309629/257924
Это становится ближе, но синтаксис все еще генерирует RSI, так как он занимает не более трех строк, чтобы сказать "C или D "в вызове:
Options options;
options.C = true;
options.D = true;
SomeOptionsUsingFunction(options);
Что мне действительно нужно, так это то, что в большинстве случаев является заменой для передачи жестко закодированных значений в существующую функцию.
Обновление № 2
https://stackoverflow.com/a/52309629/257924 также упоминает myFunction
как функцию шаблона, но я не могу использовать эту опцию, так как это означает выставление всей исходной функции, которую я изменяю в заголовок, и она слишком велика, чтобы сделатьчто.
Макет
main.cpp
содержит:
#include <stdio.h>
enum SomeEnum {
E_INVALID = 0,
E_A = 1,
E_B = 1 << 1,
E_C = 1 << 2,
E_D = 1 << 3,
};
void SomeEnumUsingFunction(SomeEnum se)
{
if (se & (E_C | E_D)) {
printf("Has: C or D\n");
}
}
void ExperimentWithSomeEnum()
{
{
printf("Attempting A\n");
SomeEnum se(E_A);
SomeEnumUsingFunction(se);
}
{
printf("Attempting C\n");
SomeEnum se(E_C);
SomeEnumUsingFunction(se);
}
{
printf("Attempting D\n");
SomeEnum se(E_D);
SomeEnumUsingFunction(se);
}
{
printf("Attempting C | D\n");
#ifdef SCENARIO_1
// This next line below is simple, but gcc errors out with:
//
// error: invalid conversion from ‘int’ to ‘SomeEnum’ [-fpermissive]
//
// GCC == c++ (GCC) 6.2.1 20160916 (Red Hat 6.2.1-3)
SomeEnum se(E_A | E_D);
SomeEnumUsingFunction(se);
#endif
#ifdef SCENARIO_2
SomeEnum se(static_cast<SomeEnum>(E_A | E_D));
SomeEnumUsingFunction(se);
#endif
#ifdef SCENARIO_3
// This is a little better but still stinks as the caller _has_ to wrap the
// value around "SomeEnum(...)" which is annoying.
SomeEnum se(SomeEnum(E_A | E_D));
SomeEnumUsingFunction(se);
#endif
#ifdef SCENARIO_4
// OOOPS: Completely defeated!! Some lazy programmer can hack in "1 << 8"
// and change SomeEnumUsingFunction without having to change the header that
// defines SomeEnum. I want to syntactically prevent them from being lazy
// and hacking around the type system to avoid recompiling "the world" that
// will necessarily occur when the header is changed.
SomeEnum se(SomeEnum(E_A | E_D | (1 << 8)));
SomeEnumUsingFunction(se);
#endif
}
}
class SomeEnumImposter
{
public:
SomeEnumImposter() : _additions(E_INVALID) {}
// Using default copy constructor.
// Using default operator=().
#define define_getter_and_setter(X) \
SomeEnumImposter & X() \
{ \
_additions = SomeEnum(_additions | E_##X); \
return *this; \
} \
bool has##X() \
{ \
return _additions & E_##X; \
}
define_getter_and_setter(A);
define_getter_and_setter(B);
define_getter_and_setter(C);
define_getter_and_setter(D);
private:
SomeEnum _additions;
};
void SomeEnumImposterUsingFunction(SomeEnumImposter se)
{
if ( se.hasC() || se.hasD() ) {
printf("Has: C or D\n");
}
}
void ExperimentWithSomeEnumImposter()
{
// Poor-mans assert():
if ( ! (sizeof(SomeEnum) == sizeof(SomeEnumImposter)) ) {
printf("%s:%d: ASSERTION FAILED: sizeof(SomeEnum) == sizeof(SomeEnumImposter)\n",__FILE__,__LINE__);
return;
}
{
printf("Attempting A\n");
SomeEnumImposterUsingFunction(SomeEnumImposter().A());
}
{
printf("Attempting C\n");
SomeEnumImposterUsingFunction(SomeEnumImposter().C());
}
{
printf("Attempting D\n");
SomeEnumImposterUsingFunction(SomeEnumImposter().D());
}
{
printf("Attempting C | D\n");
SomeEnumImposterUsingFunction(SomeEnumImposter().C().D());
}
}
struct Options {
Options() : A(false), B(false), C(false), D(false) {}
bool A;
bool B;
bool C;
bool D;
};
void SomeOptionsUsingFunction(Options option_)
{
if ( option_.C || option_.D ) {
printf("Has: C or D\n");
}
}
void ExperimentWithClassWithBoolDatamembers()
{
{
printf("Attempting A\n");
Options options;
options.A = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting C\n");
Options options;
options.C = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting D\n");
Options options;
options.D = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting C | D\n");
Options options;
options.C = true;
options.D = true;
SomeOptionsUsingFunction(options);
}
}
int main(int argc, char *argv[], char *const envp[])
{
#ifdef PLAIN_ENUM
ExperimentWithSomeEnum();
#endif
#ifdef ENUM_IMPOSTER
ExperimentWithSomeEnumImposter();
#endif
#ifdef CLASS_WITH_BOOL_DATAMEMBERS
ExperimentWithClassWithBoolDatamembers();
#endif
return 0;
}
compare.sh
содержит:
#!/bin/bash
compile_and_run () {
local define_a_macro="$1"
rm -f main.o
/usr/bin/g++ -MD -DDEBUG -g $define_a_macro -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.cpp -c -o main.o
/usr/bin/g++ -MD -DDEBUG -g $define_a_macro -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.o -L. -L/usr/lib64 -lstdc++ -o main.exe
./main.exe
}
echo
/usr/bin/g++ --version
set -e
echo
echo "PLAIN_ENUM:"
(
set -x -e
compile_and_run -DPLAIN_ENUM
)
echo
echo "ENUM_IMPOSTER:"
(
set -x -e
compile_and_run -DENUM_IMPOSTER
)
echo
echo "CLASS_WITH_BOOL_DATAMEMBERS:"
(
set -x -e
compile_and_run -DCLASS_WITH_BOOL_DATAMEMBERS
)
Работает ./compare.sh
производит этот вывод:
g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-4)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
PLAIN_ENUM:
+ compile_and_run -DPLAIN_ENUM
+ local define_a_macro=-DPLAIN_ENUM
+ rm -f main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DPLAIN_ENUM -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.cpp -c -o main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DPLAIN_ENUM -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.o -L. -L/usr/lib64 -lstdc++ -o main.exe
+ ./main.exe
Attempting A
Attempting C
Has: C or D
Attempting D
Has: C or D
Attempting C | D
ENUM_IMPOSTER:
+ compile_and_run -DENUM_IMPOSTER
+ local define_a_macro=-DENUM_IMPOSTER
+ rm -f main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DENUM_IMPOSTER -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.cpp -c -o main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DENUM_IMPOSTER -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.o -L. -L/usr/lib64 -lstdc++ -o main.exe
+ ./main.exe
Attempting A
Attempting C
Has: C or D
Attempting D
Has: C or D
Attempting C | D
Has: C or D
CLASS_WITH_BOOL_DATAMEMBERS:
+ compile_and_run -DCLASS_WITH_BOOL_DATAMEMBERS
+ local define_a_macro=-DCLASS_WITH_BOOL_DATAMEMBERS
+ rm -f main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DCLASS_WITH_BOOL_DATAMEMBERS -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.cpp -c -o main.o
+ /usr/bin/g++ -MD -DDEBUG -g -DCLASS_WITH_BOOL_DATAMEMBERS -ggdb -gstabs+ -O0 -fPIC -Wall -Werror -Wsynth -Wno-comment -Wreturn-type main.o -L. -L/usr/lib64 -lstdc++ -o main.exe
+ ./main.exe
Attempting A
Attempting C
Has: C or D
Attempting D
Has: C or D
Attempting C | D
Has: C or D