code

C ++ 함수 인수 안전성

codestyles 2020. 12. 10. 20:20
반응형

C ++ 함수 인수 안전성


동일한 유형의 여러 인수를받는 함수에서 호출자가 순서를 엉망으로 만들지 않도록 어떻게 보장 할 수 있습니까?

예를 들면

void allocate_things(int num_buffers, int pages_per_buffer, int default_value ...

그리고 나중에

// uhmm.. lets see which was which uhh..
allocate_things(40,22,80,...

일반적인 해결책은 이름이 지정된 필드가있는 구조에 매개 변수를 배치하는 것입니다.

AllocateParams p;
p.num_buffers = 1;
p.pages_per_buffer = 10;
p.default_value = 93;
allocate_things(p);

물론 필드를 사용할 필요는 없습니다. 멤버 함수 또는 원하는 것을 사용할 수 있습니다.


C ++ 11 컴파일러가있는 경우 사용자 정의 유형과 함께 사용자 정의 리터럴사용할 수 있습니다. 다음은 순진한 접근 방식입니다.

struct num_buffers_t {
    constexpr num_buffers_t(int n) : n(n) {}  // constexpr constructor requires C++14
    int n;
};

struct pages_per_buffer_t {
    constexpr pages_per_buffer_t(int n) : n(n) {}
    int n;
};

constexpr num_buffers_t operator"" _buffers(unsigned long long int n) {
    return num_buffers_t(n);
}

constexpr pages_per_buffer_t operator"" _pages_per_buffer(unsigned long long int n) {
    return pages_per_buffer_t(n);
}

void allocate_things(num_buffers_t num_buffers, pages_per_buffer_t pages_per_buffer) {
    // do stuff...
}

template <typename S, typename T>
void allocate_things(S, T) = delete; // forbid calling with other types, eg. integer literals

int main() {
    // now we see which is which ...
    allocate_things(40_buffers, 22_pages_per_buffer);

    // the following does not compile (see the 'deleted' function):
    // allocate_things(40, 22);
    // allocate_things(40, 22_pages_per_buffer);
    // allocate_things(22_pages_per_buffer, 40_buffers);
}

지금까지 두 가지 좋은 대답, 하나 더 : 또 다른 접근 방식은 가능한 한 유형 시스템을 활용하고 강력한 typedef를 만드는 것입니다. 예를 들어 boost strong typedef ( http://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html )를 사용합니다.

BOOST_STRONG_TYPEDEF(int , num_buffers);
BOOST_STRONG_TYPEDEF(int , num_pages);

void func(num_buffers b, num_pages p);

잘못된 순서의 인수로 func를 호출하면 이제 컴파일 오류가 발생합니다.

이것에 대한 몇 가지 메모. 첫째, boost의 강력한 typedef는 접근 방식에서 다소 오래된 것입니다. 가변 CRTP로 훨씬 더 좋은 일을 할 수 있고 매크로를 완전히 피할 수 있습니다. 둘째, 명시 적으로 변환해야하는 경우가 많기 때문에 분명히 이로 인해 약간의 오버 헤드가 발생합니다. 그래서 일반적으로 당신은 그것을 남용하고 싶지 않습니다. 라이브러리에서 반복해서 나오는 일에 정말 좋습니다. 일회성으로 나오는 일에는 그렇게 좋지 않습니다. 예를 들어 GPS 라이브러리를 작성하는 경우 미터 단위의 거리에 대해 강력한 double typedef가 있어야하고 나노초 단위의 Epoch를 지난 시간에 대해 강력한 int64 typedef가 있어야합니다.


(참고 : 게시물은 원래 'C'태그가 지정되었습니다.)

C99 이상에서는 @Dietrich Epp 아이디어를 확장 할 수 있습니다 . 복합 리터럴

struct things {
  int num_buffers;
  int pages_per_buffer;
  int default_value 
};
allocate_things(struct things);

// Use a compound literal
allocate_things((struct things){.default_value=80, .num_buffers=40, .pages_per_buffer=22});

Could even pass the address of the structure.

allocate_things(struct things *);

// Use a compound literal
allocate_things(&((struct things){.default_value=80,.num_buffers=40,.pages_per_buffer=22}));

You can't. That's why it is recommended to have as few function arguments as possible.

In your example you could have separate functions like set_num_buffers(int num_buffers), set_pages_per_buffer(int pages_per_buffer) etc.

You probably have noticed yourself that allocate_things is not a good name because it doesn't express what the function is actually doing. Especially I would not expect it to set a default value.


Just for completeness, you could use named arguments, when your call becomes.

void allocate_things(num_buffers=20, pages_per_buffer=40, default_value=20);
// or equivalently
void allocate_things(pages_per_buffer=40, default_value=20, num_buffers=20);

However, with the current C++ this requires quite a bit of code to be implemented (in the header file declaring allocate_things(), which must also declare appropriate external objects num_buffers etc providing operator= which return a unique suitable object).

---------- working example (for sergej)

#include <iostream>

struct a_t { int x=0; a_t(int i): x(i){} };
struct b_t { int x=0; b_t(int i): x(i){} };
struct c_t { int x=0; c_t(int i): x(i){} };

// implement using all possible permutations of the arguments.
// for many more argumentes better use a varidadic template.
void func(a_t a, b_t b, c_t c)
{ std::cout<<"a="<<a.x<<" b="<<b.x<<" c="<<c.x<<std::endl; }
inline void func(b_t b, c_t c, a_t a) { func(a,b,c); }
inline void func(c_t c, a_t a, b_t b) { func(a,b,c); }
inline void func(a_t a, c_t c, b_t b) { func(a,b,c); }
inline void func(c_t c, b_t b, a_t a) { func(a,b,c); }
inline void func(b_t b, a_t a, c_t c) { func(a,b,c); }

struct make_a { a_t operator=(int i) { return {i}; } } a;
struct make_b { b_t operator=(int i) { return {i}; } } b;
struct make_c { c_t operator=(int i) { return {i}; } } c;

int main()
{
  func(b=2, c=10, a=42);
}

Are you really going to try to QA all the combinations of arbitrary integers? And throw in all the checks for negative/zero values etc?

Just create two enum types for minimum, medium and maximum number of buffers, and small medium and large buffer sizes. Then let the compiler do the work and let your QA folks take an afternoon off:

allocate_things(MINIMUM_BUFFER_CONFIGURATION, LARGE_BUFFER_SIZE, 42);

Then you only have to test a limited number of combinations and you'll have 100% coverage. The people working on your code 5 years from now will only need to know what they want to achieve and not have to guess the numbers they might need or which values have actually been tested in the field.

It does make the code slightly harder to extend, but it sounds like the parameters are for low-level performance tuning, so twiddling the values should not be perceived as cheap/trivial/not needing thorough testing. A code review of a change from allocate_something(25, 25, 25);

...to

allocate_something(30, 80, 42);

...will likely get just a shrug/blown off, but a code review of a new enum value EXTRA_LARGE_BUFFERS will likely trigger all the right discussions about memory use, documentation, performance testing etc.

참고URL : https://stackoverflow.com/questions/38836690/c-function-argument-safety

반응형