问题摘要
是否有一种类型安全的参数替换为int
,但将该值解释为一组逐位值表示互斥操作,但阻止调用方从使用幻数来表示这些运算?这需要使用GCC 4.8.5,无需升级。
详细问题
我继承了一个旧的C风格C++函数,它接受int
指示要执行的某些操作的参数。这些操作是当前作为0x1 | 0x2
传递给函数,并展开整个代码库。在我看来,这是一个美丽的不可维护的情况:它要求呼叫者使用幻数,因此,迫使开发人员读取整个函数实现只是为了了解所请求的操作。
因此,我试图更改接口,以使用清晰的名称指示要执行的功能的请求,同时除了特定参数类型的更改。我不能,在短期内,重构函数以使用适当的多态方法其中操作表示为单独的类,作为这些神奇的数字已经变得太普遍了——在一个单一的数字中重构操作。我想要求添加到函数中的操作需要一些中心类型更改为清楚地指示函数必须执行的操作,而不是允许不耐烦的开发人员执行侵入另一个值(例如0x6
)并更改的奢侈使用ever检查该值的函数内部增加了不可维护的条件逻辑。在我下面的模型中我定义了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 Dn");
}
}
void ExperimentWithSomeEnum()
{
{
printf("Attempting An");
SomeEnum se(E_A);
SomeEnumUsingFunction(se);
}
{
printf("Attempting Cn");
SomeEnum se(E_C);
SomeEnumUsingFunction(se);
}
{
printf("Attempting Dn");
SomeEnum se(E_D);
SomeEnumUsingFunction(se);
}
{
printf("Attempting C | Dn");
#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 Dn");
}
}
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 An");
SomeEnumImposterUsingFunction(SomeEnumImposter().A());
}
{
printf("Attempting Cn");
SomeEnumImposterUsingFunction(SomeEnumImposter().C());
}
{
printf("Attempting Dn");
SomeEnumImposterUsingFunction(SomeEnumImposter().D());
}
{
printf("Attempting C | Dn");
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 Dn");
}
}
void ExperimentWithClassWithBoolDatamembers()
{
{
printf("Attempting An");
Options options;
options.A = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting Cn");
Options options;
options.C = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting Dn");
Options options;
options.D = true;
SomeOptionsUsingFunction(options);
}
{
printf("Attempting C | Dn");
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
选项类
创建一个像这样的选项结构/类,并用作函数的输入
struct Options {
bool option1;
bool option2;
bool option3;
};
如果你担心填充,你可以使用c++位字段(尽管这会有其他人"滥用"它的风险)。
选项类别详细使用(澄清后)
要用作一个liner/inline,您可以使用聚合初始化(需要c++11,但默认情况下已启用):
struct Options {
bool option1;
bool option2;
bool option3;
};
void myFunc(Options options) {
}
void test() {
myFunc(Options{ false, false, true });//OK
myFunc({ false, false, true });//also OK
}
如果没有,或者您更喜欢不使用聚合初始化,那么您可以只编写一个接受所有选项的普通构造函数(这增加了能够提供一些默认值的灵活性)。
struct Options {
Options(bool option_1 = true, bool option_2 = false);
//...
};
myFunc(Options( false ));//using constructor
如果您有很多bool,那么以某种方式命名实际选项可能是个好主意,例如使用enums:
struct Options {
enum OptionA {
off = false,
on = true,
};
enum OptionB {
do1,
do2,
do3
};
OptionA optionA;
OptionB optionB;
bool optionC;
};
基于模板策略的设计
更改函数以获取一个或多个模板参数并提供不同的选项。我认为这就是你想要的解决方案。示例:
struct Options {
bool option1;
bool option2;
bool option3;
};
struct OptionA1 {
};
struct OptionA2 {
};
struct OptionSetA12 : public OptionA1, public OptionA2 {
};
OptionA1 optionSetA1;
OptionA2 optionSetA2;
OptionSetA12 optionSetA12;
struct OptionB1 {
};
struct OptionB2 {
};
OptionB1 optionB1;
OptionB2 optionB2;
template<class OptionA_T, class OptionB_T>
void myFunction(OptionA_T optionA_t, OptionB_T optionB_t, int someInput) {
if (boost::is_convertible<Option_T, Option1>::value) {
//do whatever option 1
}
}
//specialized:
template<class OptionB_T>
void myFunction(OptionSetA12 optionSetA12, OptionB_T optionB_t, int someInput) {
//specialized version for OptionSetA12, still has OptionB_T as parameter
}
void test() {
myFunction(optionSetA12, optionB2, 0);
}
正如你所看到的,这样做会给你很大的灵活性,不会有被滥用的风险,也不会有什么负面影响。随着时间的推移,您可能可以摆脱is_convertible
条件,并将代码放入策略类本身。请参见示例https://en.wikipedia.org/wiki/Policy-based_design
这不是一个答案,只是我留下的状态:
我最终选择了:
const unsigned int XXX = 1;
const unsigned int YYY = 1 << 1;
const unsigned int ZZZ = 1 << 2;
等等。主要原因是,我在实践中发现,"SCENARIO_3"的问题太麻烦了,以至于每次将生成的整数值提供给使用它的函数时,都无法要求开发人员将上述的按位-OR组合转换为枚举类型。
我最终达成的妥协是:
void that_function(int check);
...
that_function(YYY | ZZZ);
至少有了上述折衷方案,他们不会像以前那样对数字进行硬编码:
that_function(0x0002 | 0x0004);