Little Jay

[C++] Attributes 본문

Univ/Study

[C++] Attributes

Jay, Lee 2022. 2. 3. 17:28

Clion을 사용하면서 재미있는 부분을 발견했습니다.

오른쪽에 IntelliSense가 추천해주는 코딩 스타일에서 [[nodiscard]]를 사용하는것이 권장된다고 나와있습니다.

 

함수앞에 [[nodiscard]]를 붙일 수 있는데 이런것을 Attribute라고 합니다.

처음보는 스타일에 당황하긴 했지만 C++ 11부터 이런 스타일을 지원했으며,

컴파일시 최적화 부분에서 이를 유용하게 사용할 수 있겠습니다.

본 포스팅에서는 이 Attribute에 대해 정리 해보겠습니다.

 

Attribute

직역하면 속성이라는 의미이다.

C++ 전역에서 사용이 가능하며(유형, 변수, 함수, 이름, 코드 블록 앞에서 사용 가능),

이는 컴파일할때 특정 메세지를 생성하거나 컴파일러가 특정 동작을 수행할 수 있게 해줍니다. 

이는 필수적으로 붙이는 속성은 아닙니다.

그러나 이를 통해 최적화, 다양한 동작들을 할 수 있다는 점에서 유의미합니다.

 

Syntax

[[ attr ]]
[[ attr1, attr2, attr3(args) ]]
[[ attr-list ]]
[[ using attr-namespace: attr-list]] // since C++ 17

위와 같이 bracket 두개를 사용합니다.

Attributes들을 열거해서 사용할 수도 있고 특정 namespace안에서 사용할 수 있도록 해줍니다.

 

[[ noreturn ]] c++11

함수가 아무것도 return하지 않는다는 것을 알려줍니다. 

함수의 선언 부분에서만 declare 할 수 있으며, 함수가 실제로 반환되는 경우 warning을 일으킵니다.

여기서 warning이란 컴파일 하는데에는 문제가 없지만, 권장되지는 않는다라는 의미입니다.

이를 통해 컴파일러가 최적화를 수행할 수 있습니다.

 

호출에 관련된 임시 상태들을 저장하고 불러올 필요가 없습니다. 

호출자에게 돌아오지 않기에, 불필요한 블럭 내 하위 코드들을 dead-code로써 지워버릴 수가 있다는 것이죠.

 

또한 [[ noreturn ]]으로 선언된 함수가 어떠한 값(void포함) 반환하려 할 경우의 결과는 미정(undefined)입니다.

 

VS에서 DEBUG 모드에서 수행할 경우 아래의 코드가 잘 수행될 수 있지만, r

elease모드에서는 foo와 bar는 실행되지 않습니다.

#include <iostream>
using namespace std;

[[ noreturn ]] void trw() {
    throw "error";
}

[[ noreturn ]] void func(int a) {
    // do nothing; 
    // 정상적인 코드
}
 
[[ noreturn ]] int foo() {
    return 1;
    // warning 발생
}

int bar [[ noreturn ]] (int n) {
    return 2;
    // warning 발생
}

int main() {
    func(10);
    //deadcode
    cout << foo();
    cout << bar(10);

    return 0;
}

[[ carries_dependency ]] c++11

이는 std::memory_order의 종속성 체인이 함수 내부 및 외부로 전파되어 컴파일러가 불필요한 메모리 펜스 명령을 건너뛸 수 있음을 나타냅니다.

cppreference에 이렇게 나와 있어 번역은 했지만 아직 예시같은게 없어 감이 잘 안옵니다.

추후에 수정하겠습니다.

 

[[deprecated]], [[deprecated("message")]] c++14

사용은 가능하나 권장되지는 않는 attribute입니다.

함수를 사용 하기에 적합 하지 않을 수 있음을 의미합니다.

컴파일러는 클라이언트 코드에서 함수를 호출 하려고 할 때, 사용자에게 warning 메세지를 전달합니다.

위의 "message"로 전달하고 싶은 메세지를 전달할 수 있습니다. 
클래스의 선언, typedef 이름, 변수, 비정적 데이터 멤버, 함수, 네임 스페이스, 열거형, 열거자 또는 템플릿 특수화에 적용할 수 있습니다.

라이브러리 제작자들에게 많이 사용될 수 있는 코드입니다.

cppreference 사이트에 예시가 따로 없어 예시는 다른 블로그에서 참조했습니다.

#include <iostream>
using namespace std;

[[deprecated]] void foo(int i) {
    if (i > 0)
        throw "positive";
}

// note: 'bar' 선언을 참조하십시요
[[deprecated("Will be removed in the next version")]] void bar(int i) {
    if (i > 0)
        throw "positive";
}

int main()
{
    // warning C4996: 'foo': deprecated로 선언되었습니다.
    foo(1);

    // warning C4996: 'goo': Will be removed in the next version
    bar(1);
}

[[ nodiscard ]] c++17

함수의 반환 값이 삭제 되지 않도록 지정합니다.

만약 return값이 버려질 경우 컴파일 경고를 발생시킵니다.

그러나 강제로 이를 void로 형변환을 하게되면 경고가 나타나지 않습니다.

[[ nodiscard ]] int foo(int x) {
    return x * 2;
}

int main()
{
    // warning C4834: 'nodiscard' 특성이 포함된 함수의 반환 값을 버리는 중
    foo(1);
    
    (void)foo(2);

    return 0;
}

[[ maybe_unused ]] c++17

[[ nodiscard ]]와는 반대의 개념입니다.

C++ 컴파일러 대부분 사용되지 않는 변수나 함수에 대해서 warning을 불러줍니다
그러나 [[ maybe_unused ]]를 사용하게 되면 이러한 warning을 보여주지 않습니다.
#include <cassert>

[[maybe_unused]] void foo([[maybe_unused]] bool thing1,
    [[maybe_unused]] bool thing2) {
    [[maybe_unused]] bool bar = thing1 && thing2;
    assert(bar); // in release mode, assert is compiled out, and b is unused
               // no warning because it is declared [[maybe_unused]]
} 

int main() { foo(true, true); }

[[ fallthough ]] c++17

switch문에서 사용가능한 attribute입니다. 

이는 case, default 전에 명시되어야합니다.

직역을 하게 되면 '그대로 내려간다' 정도로 해석할 수 있겠습니다'

switch문에서 의도적으로 break를 넣지 않을 수 있는데 컴파일러에서 이를 경고를 출력하게됩니다.

이 속성은 컴파일러에게 그러한 경고를 발생시키지 않겠다고 미리 알려주는 역할을 해줍니다. 

아래의 예시를 실행시켜보시면 쉽게 이해가 되실겁니다.

void f(int n) {
  void g(), h(), i();
  switch (n) {
    case 1:
    case 2:
      g();
     [[fallthrough]];
    case 3: // no warning on fallthrough
      h();
    case 4: // compiler may warn on fallthrough
      if(n < 3) {
          i();
          [[fallthrough]]; // OK
      }
      else {
          return;
      }
    case 5:
      while (false) {
        [[fallthrough]]; // ill-formed: next statement is not part of the same iteration
      }
    case 6:
      [[fallthrough]]; // ill-formed, no subsequent case or default label
  }
}

[[ likely ]], [[unlikely]]  c++20

컴파일러가 해당 문을 포함하는 실행 경로가 해당 문을 포함하지 않는 대체 실행 경로보다

더 높거나 더 낮은 경우에 최적화하도록 허용합니다.

아래의 예시를 보시면 바로 이해가 가실겁니다.

 i == 1의 경우에는 case 2에 들어가더라도 영향을 미치지 않게 되는 것이죠.

#include <iostream>
using namespace std;

int f(int i)
{
    switch (i)
    {
    case 1: 
        cout << "fall through excetued and go to case 2";
        [[fallthrough]];
    [[likely]] case 2: return 1;
    }
    return 2;
}

int main() {
    cout << f(1);
}

unlikely는 그 반대의 개념으로 이해하시면 됩니다. 

이러한 likely와 unlikely의 attribute의 특성은 시간적인 측면에서 유의미합니다.

아래의 코드를 보시면 이해가 되실겁니다.

#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <random>
 
namespace with_attributes {
constexpr double pow(double x, long long n) noexcept {
    if (n > 0) [[likely]]
        return x * pow(x, n - 1);
    else [[unlikely]]
        return 1;
}
constexpr long long fact(long long n) noexcept {
    if (n > 1) [[likely]]
        return n * fact(n - 1);
    else [[unlikely]]
        return 1;
}
constexpr double cos(double x) noexcept {
    constexpr long long precision{16LL};
    double y{};
    for (auto n{0LL}; n < precision; n += 2LL) {
        [[likely]] y += pow(x, n) / (n & 2LL ? -fact(n) : fact(n));
    }
    return y;
}
}  // namespace with_attributes
 
namespace no_attributes {
constexpr double pow(double x, long long n) noexcept {
    if (n > 0)
        return x * pow(x, n - 1);
    else
        return 1;
}
constexpr long long fact(long long n) noexcept {
    if (n > 1)
        return n * fact(n - 1);
    else
        return 1;
}
constexpr double cos(double x) noexcept {
    constexpr long long precision{16LL};
    double y{};
    for (auto n{0LL}; n < precision; n += 2LL) {
        y += pow(x, n) / (n & 2LL ? -fact(n) : fact(n));
    }
    return y;
}
}  // namespace no_attributes
 
double gen_random() noexcept {
    static std::random_device rd;
    static std::mt19937 gen(rd());
    static std::uniform_real_distribution<double> dis(-1.0, 1.0);
    return dis(gen);
}
 
volatile double sink{}; // ensures a side effect
 
int main() {
    for (const auto x : {0.125, 0.25, 0.5, 1. / (2 << 25)}) {
        std::cout
            << std::setprecision(53)
            << "x = " << x << '\n'
            << std::cos(x) << '\n'
            << with_attributes::cos(x) << '\n'
            << (std::cos(x) == with_attributes::cos(x) ? "equal" : "differ") << '\n';
    }
 
    auto benchmark = [](auto fun, auto rem) {
        const auto start = std::chrono::high_resolution_clock::now();
        for (auto size{1ULL}; size != 10'000'000ULL; ++size) {
            sink = fun(gen_random());
        }
        const std::chrono::duration<double> diff =
            std::chrono::high_resolution_clock::now() - start;
        std::cout << "Time: " << std::fixed << std::setprecision(6) << diff.count()
                  << " sec " << rem << std::endl;
    };
 
    benchmark(with_attributes::cos, "(with attributes:)");
    benchmark(no_attributes::cos, "(without attributes)");
    benchmark(cos, "(std::cos)");
}

[[ no_unique_address ]]

이거를 이해하는데 시간이 좀 걸렸습니다.

https://stackoverflow.com/questions/62784750/what-is-the-new-feature-in-c20-no-unique-address

stackoverflow에서 보다 정확한 이해를 하실 수 있습니다.

 

메모리 최적화를 수행합니다.

 

c++에서는 0을 허용하지 않습니다.

이것이 어떤 말인가 하면, 어떤 data에 대해서 항상 sizeof(obj)는 0이상이 됩니다.

이게 어떤 문제를 야기하게 되나면, 

첫 번째로는 stateless objects(멤버가 없는 classes/structs)가 사용될 경우에 메모리가 낭비되고,

다음으로는 size가 0인 빈 배열을 선언할 수 없습니다.

물론 std::array를 사용하면 두 번째의 배열 문제는 해결할 수 있습니다.

이때 [[ no_unique_address ]]는 이 문제를 해결하게 되는것이죠.

물론 이는 근본적인 해결책이라고는 할 수 없습니다. 

사용자의 요청이 있을때만 사용할 수 있는 attribute이기 때문이죠.

 

아래의 코드를 보시면 이해가 쉽게 되실겁니다.

struct Empty {}; // empty class

struct X {
    int i;
    Empty e;
};

struct Y {
    int i;
    [[no_unique_address]] Empty e;
};


int main()
{
    // 빈 클래스라고 할지라고 항상 size는 1 이상이 됩니다.
    static_assert(sizeof(Empty) >= 1);

    // 그렇기 떄문에 여기서 에러가 발생되지 않습니다.
    static_assert(sizeof(X) >= sizeof(int) + 1);

    // [[no_unique_address]]를 통해 메모리 최적화를 해줄 수 있습니다.
    std::cout << "sizeof(Y) == sizeof(int) is " << std::boolalpha
        << (sizeof(Y) == sizeof(int)) << '\n';

}

 

마무리

Attribute는 필수적인 조건이 아닙니다.

이러한 것이 있다 정도로 이해하시고 최신버전에 맞는 코딩을 하고싶다면 참조해도 될것 같습니다.

감사합니다.

 

출처

https://en.cppreference.com/w/cpp/language/attributes

http://egloos.zum.com/sweeper/v/3201377

 

 

Comments