Поиск безопасной для замены типа аргумента int функции C ++, представляющей одну или несколько операций - PullRequest
0 голосов
/ 13 сентября 2018

Сводка вопроса

Существует ли безопасная по типу замена аргументов для функций, которые принимают 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

Ответы [ 2 ]

0 голосов
/ 26 октября 2018

Это не ответ, просто состояние того, где я оставил это:

Я закончил с:

const unsigned int XXX = 1;
const unsigned int YYY = 1 << 1;
const unsigned int ZZZ = 1 << 2;

и т.д.. Основная причина заключается в том, что на практике я обнаружил, что проблема со «SCENARIO_3» была слишком обременительной, чтобы попросить разработчиков приводить приведенные выше побитовые ИЛИ комбинации к типу enum каждый раз, когда полученное целочисленное значение передается функции, которая использует его.

В результате я получил этот компромисс:

void that_function(int check);
...
that_function(YYY | ZZZ);

По крайней мере, с указанным выше компромиссом, они не являются такими жесткими кодами, как это было раньше:

that_function(0x0002 | 0x0004);
0 голосов
/ 13 сентября 2018

Опции класса

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

struct Options {
    bool option1;
    bool option2;
    bool option3;
};

Если вас беспокоит заполнение, вы можете использовать битовое поле c ++ (хотя это рискует, что другие «злоупотребят» им).

Опция Класс Детальное использование (после уточнения)

Для использования в качестве одного вкладыша / in-line вы можете использовать агрегатную инициализацию (требуется 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

Если у вас много bools, было бы неплохо назвать фактические параметры каким-либо образом, например, используя enums:

struct Options {
    enum OptionA {
        off = false,
        on = true,
    };
    enum OptionB {
        do1,
        do2,
        do3
    };

    OptionA optionA;
    OptionB optionB;
    bool optionC;
};

Шаблон на основе политики (как) дизайн

Измените функцию, чтобы получить один или несколько параметров шаблона и предоставить различные параметры. Я думаю, что это решение, к которому вы стремитесь. Пример: * +1021 *

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

...