C++ 속성(Attributes): [[nodiscard]]·[[deprecated]]·[[likely]]·[[fallthrough]] 실전 사용법

이 글의 핵심

속성은 코드 동작을 바꾸지 않고 컴파일러에 의도를 전달하는 수단이라, 잘 쓰면 리뷰에서 놓칠 실수를 빌드 단계에서 잡아 줍니다. 반면 likely/unlikely를 근거 없이 붙이거나 fallthrough를 잘못된 위치에 두면 효과가 없거나 오히려 경고가 늘어납니다. 컴파일러별 지원 차이와 속성을 조합하는 방법도 함께 다룹니다.

Attributes란?

속성(attribute)은 C++11에서 도입된 [[...]] 문법으로, 컴파일러에게 코드에 대한 추가 정보를 전달하는 표준화된 방법입니다. 반환값을 무시하면 경고를 내게 하거나, API가 곧 사라진다는 사실을 알리거나, 어느 분기가 더 자주 실행되는지 힌트를 주는 데 씁니다. 예전에는 같은 일을 GCC의 __attribute__나 MSVC의 __declspec 같은 컴파일러별 확장으로 했기 때문에 #ifdef가 필요했지만, 표준 속성은 그대로 여러 컴파일러에서 쓸 수 있습니다.

// 컴파일러별 확장: 비표준
#ifdef __GNUC__
    __attribute__((warn_unused_result))
#endif
int compute();

// 표준 속성 (C++17 이상)
[[nodiscard]] int compute();

속성의 가장 중요한 성질은 대부분 프로그램의 의미를 바꾸지 않는다는 것입니다. [[nodiscard]]를 지워도 코드는 똑같이 동작하고, 달라지는 것은 컴파일러가 내는 경고와 최적화 판단뿐입니다(단, [[noreturn]]이나 C++20 [[no_unique_address]]처럼 계약을 어기면 정의되지 않은 동작이 되거나 객체 레이아웃이 바뀌는 속성도 있습니다). 또 표준은 구현이 모르는 속성을 무시하도록 정하고 있어서, [[gnu::always_inline]]을 MSVC에서 컴파일하면 에러 대신 “알 수 없는 속성” 경고만 나옵니다. 이 규칙 덕분에 #ifdef 없이도 여러 컴파일러에서 빌드되는 코드를 쓸 수 있지만, 속성 이름에 오타를 내도([[nodiscrad]]) 경고 하나로 조용히 무시된다는 뜻이기도 합니다.

[[nodiscard]]

[[nodiscard]](C++17)를 붙인 함수의 반환값을 버리면 컴파일러가 경고를 냅니다. 에러 코드나 새로 만든 리소스 핸들을 반환하는 함수에 주로 씁니다.

// 반환값 무시 시 경고
[[nodiscard]] int compute() {
    return 42;
}

int main() {
    compute();  // 경고: 반환값 무시
    
    int result = compute();  // OK
}

실패를 반환값으로 알리는 함수가 대표적인 예입니다.

// 에러 코드
[[nodiscard]] bool saveFile(const std::string& path) {
    // 저장 로직
    return true;
}

// ❌ 에러 무시
saveFile("data.txt");  // 경고

// ✅ 에러 확인
if (!saveFile("data.txt")) {
    std::cerr << "저장 실패\n";
}

[[nodiscard]]가 가장 효과적인 곳은 반환값을 무시하면 버그가 되는 함수입니다. 실패를 bool이나 에러 코드로 알리는 함수, 새로 할당한 자원을 돌려주는 함수, 그리고 이름이 부수 효과처럼 보이지만 실제로는 값을 돌려주는 함수가 대표적입니다. 마지막 경우의 유명한 예가 std::vector::empty()입니다. “비운다”로 오해해 v.empty();만 호출하고 벡터가 비워졌다고 믿는 실수가 흔했기 때문에, C++20 표준 라이브러리는 empty()에 [[nodiscard]]를 붙였습니다. 새 컴파일러에서 기존 코드에 이 경고가 나온다면 대부분 실제 버그입니다.

반대로 모든 함수에 붙이면 경고가 너무 많아져 팀이 경고를 무시하는 습관이 생깁니다. std::map::insert처럼 반환값이 있지만 대부분 무시해도 되는 함수에는 붙이지 않는 것이 좋습니다. C++20부터는 [[nodiscard("저장 실패를 확인하세요")]]처럼 이유를 적을 수 있어, 경고 메시지만 보고도 왜 반환값이 중요한지 알 수 있습니다.

[[deprecated]]

#include <iostream>
using namespace std;

// 사용 중단 경고
[[deprecated("use newFunc instead")]]
void oldFunc() {
    cout << "구식 함수" << endl;
}

void newFunc() {
    cout << "새 함수" << endl;
}

int main() {
    oldFunc();  // 경고: deprecated
    newFunc();  // OK
}

[[deprecated]](C++14)는 API를 한 번에 지우지 않고 단계적으로 바꿀 수 있게 해 줍니다. 새 함수를 추가하고 옛 함수에 deprecated를 붙여 한두 릴리스 동안 경고를 내보낸 뒤, 사용처가 사라지면 삭제하는 순서입니다. 메시지에는 무엇으로 바꿔야 하는지를 적어야 경고를 본 사람이 바로 고칠 수 있습니다.

실무에서 부딪히는 문제는 -Werror입니다. 경고를 에러로 처리하는 빌드에서는 라이브러리가 함수를 deprecated로 바꾸는 순간 사용자의 빌드가 깨집니다. 라이브러리를 업그레이드했더니 갑자기 빌드가 안 된다는 상황의 흔한 원인이라, -Werror를 쓰는 프로젝트는 -Wno-error=deprecated-declarations로 deprecated 경고만 에러에서 제외해 두는 경우가 많습니다. 또 deprecated 함수를 구현하는 라이브러리 자신의 코드(테스트 등)에서도 경고가 나므로, 그 부분에서는 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"나 MSVC의 #pragma warning(disable: 4996)으로 국소적으로 끕니다.

[[maybe_unused]]

#include <iostream>
using namespace std;

void func([[maybe_unused]] int debug) {
    #ifdef DEBUG
        cout << "디버그: " << debug << endl;
    #endif
    // debug 미사용 시 경고 없음
}

int main() {
    [[maybe_unused]] int x = 10;
    // x 미사용 시 경고 없음
}

[[maybe_unused]]는 빌드 설정에 따라 쓰이기도 하고 안 쓰이기도 하는 변수에 붙입니다. 가장 흔한 경우는 assert에서만 쓰는 변수입니다. auto ok = init(); assert(ok);는 NDEBUG로 빌드하면 assert가 사라져 ok가 사용되지 않는다는 경고가 나오는데, [[maybe_unused]] auto ok = init();로 쓰면 디버그·릴리스 모두 조용합니다. 예전에 쓰던 (void)x; 캐스트와 효과는 같지만, 선언에 붙어 있어 의도가 더 분명합니다.

[[likely]] / [[unlikely]] (C++20)

int process(int x) {
    if (x > 0) [[likely]] {
        // 대부분 이 경로
        return x * 2;
    } else [[unlikely]] {
        // 드문 경로
        return 0;
    }
}

[[likely]]는 CPU의 분기 예측기에 직접 힌트를 주는 것이 아닙니다. 현대 x86·ARM CPU는 소프트웨어 힌트를 거의 쓰지 않고 실행 이력을 보고 스스로 예측합니다. 속성이 바꾸는 것은 컴파일러의 코드 배치입니다. 자주 실행되는 경로를 분기 바로 다음에 이어 붙여 명령어 캐시를 효율적으로 쓰고, 드문 경로는 함수 끝이나 별도 영역으로 밀어냅니다. 인라인 판단이나 레지스터 할당도 자주 실행되는 쪽에 유리하게 바뀔 수 있습니다.

그래서 효과는 대개 작고, 예측이 틀리면 오히려 손해입니다. 이미 프로파일 기반 최적화(PGO, GCC의 -fprofile-use, Clang의 -fprofile-instr-use)를 쓰고 있다면 실제 실행 데이터가 더 정확하므로 수동 힌트는 거의 의미가 없습니다. 에러 처리 분기처럼 “드물다”는 것이 설계상 분명한 곳에 [[unlikely]]를 붙이는 정도가 안전한 사용법입니다. C++20 이전에는 GCC·Clang의 __builtin_expect(cond, 1)로 같은 효과를 냈고, 리눅스 커널의 likely()/unlikely() 매크로가 이것을 감싼 것입니다.

[[fallthrough]]

void process(int x) {
    switch (x) {
        case 1:
            cout << "1" << endl;
            [[fallthrough]];  // 의도적 fall-through
        case 2:
            cout << "1 또는 2" << endl;
            break;
        case 3:
            cout << "3" << endl;
            // [[fallthrough]];  // 여기 두면 잘못된 코드: 뒤에 이어질 case 레이블이 없음
    }
}

switch에서 break를 빠뜨리는 것은 C 시절부터 가장 흔한 버그 중 하나라, GCC·Clang은 -Wimplicit-fallthrough(GCC는 -Wextra에 포함)로 fall-through를 경고합니다. 문제는 의도적인 fall-through도 경고가 난다는 것이고, [[fallthrough]];는 “여기는 일부러 이어지게 했다”고 표시해 이 경고를 끄는 수단입니다. 예전에는 // fall through 같은 주석으로 표시했는데, 컴파일러가 인식하는 주석 형식이 제각각이라 표준 속성이 만들어졌습니다.

[[fallthrough]];(C++17)는 다음 case 레이블 바로 앞의 문장이어야 합니다. 마지막 case처럼 뒤에 이어질 레이블이 없는 곳에 두면 표준상 잘못된(ill-formed) 코드이고, 컴파일러에 따라 에러나 경고가 납니다(Clang은 에러로 처리합니다). 또 case 사이에 문장이 하나도 없는 경우(case 1: case 2: ...)는 원래 경고 대상이 아니므로 속성이 필요 없습니다.

[[noreturn]]

#include <iostream>
#include <string>
using namespace std;

[[noreturn]] void fatal(const string& msg) {
    cerr << "치명적 에러: " << msg << endl;
    exit(1);
}

int main() {
    if (error) {
        fatal("에러 발생");
        // 여기 도달 안함
    }
}

[[noreturn]]은 컴파일러에게 “이 함수는 절대 호출자에게 돌아오지 않는다”고 약속합니다. 컴파일러는 이 정보로 호출 이후 코드를 도달 불가능한 것으로 보고 제거하거나, 값을 반환해야 하는 함수에서 fatal() 호출 뒤에 return이 없어도 “반환값 없음” 경고를 내지 않습니다. 약속은 개발자가 지켜야 합니다. [[noreturn]] 함수가 실제로 반환하면 정의되지 않은 동작이며, 예를 들어 로깅만 하고 조건에 따라 반환하도록 나중에 수정했다면 호출자 쪽 코드가 이상하게 동작할 수 있습니다. 반환 대신 예외를 던지는 것은 허용됩니다. std::exit, std::abort, std::terminate는 표준 라이브러리에서 이미 [[noreturn]]으로 선언되어 있습니다.

실전 예시

예시 1: 에러 처리

#include <string>
using namespace std;

class [[nodiscard]] Result {
private:
    bool success;
    string message;
    
public:
    Result(bool s, string m) : success(s), message(m) {}
    
    bool isOk() const { return success; }
    string getMessage() const { return message; }
};

Result saveFile(const string& filename) {
    // 파일 저장 로직
    return Result(true, "저장 성공");
}

int main() {
    saveFile("test.txt");  // 경고: 반환값 무시
    
    auto result = saveFile("test.txt");  // OK
    if (!result.isOk()) {
        cout << result.getMessage() << endl;
    }
}

[[nodiscard]]를 타입에 붙이면 그 타입을 값으로 반환하는 모든 함수가 자동으로 nodiscard가 됩니다. 함수마다 붙이는 것을 잊을 걱정이 없고, 새로 추가되는 함수에도 적용되므로 Result나 에러 코드 enum처럼 “절대 무시하면 안 되는” 타입에 가장 효과적입니다. 다만 참조나 포인터로 반환하는 경우(Result& getLast())에는 적용되지 않습니다.

이 방식이 막을 수 있는 것은 반환값을 통째로 버리는 경우뿐이라는 점도 알아 두어야 합니다. auto result = saveFile(...)로 받아 놓고 isOk()를 확인하지 않는 코드는 경고가 나지 않습니다. 실패 확인을 강제하고 싶다면 소멸자에서 확인 여부를 검사하는 디버그용 단언을 넣거나, 값에 접근하려면 반드시 성공 여부를 거치게 하는 std::expected(C++23) 같은 타입을 쓰는 편이 더 강력합니다.

예시 2: API 버전 관리

#include <iostream>
using namespace std;

class API {
public:
    [[deprecated("use processV2 instead")]]
    void processV1(int data) {
        cout << "V1: " << data << endl;
    }
    
    void processV2(int data, bool flag = false) {
        cout << "V2: " << data << ", " << flag << endl;
    }
};

int main() {
    API api;
    
    api.processV1(10);  // 경고
    api.processV2(10);  // OK
}

예시 3: 최적화 힌트

#include <random>
#include <iostream>

using namespace std;

int main() {
    random_device rd;
    mt19937 gen(rd());
    uniform_int_distribution<> dis(1, 100);
    
    int x = dis(gen);
    
    if (x > 50) [[likely]] {
        // 50% 확률 (likely)
        cout << "큰 수" << endl;
    } else [[unlikely]] {
        // 50% 확률 (unlikely는 부적절)
        cout << "작은 수" << endl;
    }
    
    // 올바른 사용
    if (x > 95) [[unlikely]] {
        // 5% 확률
        cout << "매우 큰 수" << endl;
    }
}

50% 분기에 [[likely]]를 붙이는 것은 틀린 정보는 아니지만 쓸모가 없습니다. 어느 쪽이 더 자주 실행되는지에 대한 정보가 없으니 컴파일러가 배치를 바꿀 근거도 없습니다. 이런 분기는 CPU 분기 예측기 입장에서도 예측이 불가능해 매번 절반 가까이 틀리는데, 그 비용은 속성으로 줄일 수 없습니다. 진짜로 성능이 문제라면 분기 자체를 없애는(branchless) 방향으로 코드를 바꿔야 합니다.

예시 4: 스위치 fall-through

#include <iostream>
using namespace std;

enum Command {
    CMD_INIT,
    CMD_START,
    CMD_STOP,
    CMD_CLEANUP
};

void execute(Command cmd) {
    switch (cmd) {
        case CMD_INIT:
            cout << "초기화" << endl;
            [[fallthrough]];
        case CMD_START:
            cout << "시작" << endl;
            break;
        case CMD_STOP:
            cout << "중지" << endl;
            [[fallthrough]];
        case CMD_CLEANUP:
            cout << "정리" << endl;
            break;
    }
}

int main() {
    execute(CMD_INIT);
    // 초기화
    // 시작
}

이 예제는 “초기화한 뒤 시작도 한다”, “중지한 뒤 정리도 한다”는 순서 관계를 fall-through로 표현했습니다. 이렇게 단계가 누적되는 상태 전이에서는 fall-through가 코드 중복을 줄여 주지만, 나중에 누군가 CMD_START와 CMD_INIT 사이에 새 case를 끼워 넣으면 초기화 뒤에 엉뚱한 명령이 실행됩니다. [[fallthrough]]는 적어도 “이 연결은 의도된 것”이라는 표시를 남기므로, 리뷰어가 case 순서를 바꿀 때 한 번 더 확인하게 만듭니다. 연결 관계가 복잡해지면 init(); start();처럼 함수 호출로 명시하는 편이 안전합니다.

속성이 만들어 내는 경고를 직접 확인하기

속성은 컴파일러가 어떤 메시지를 내는지 알아야 쓸모가 있습니다. 환경은 Ubuntu 24.04, g++ 13.3, -std=c++20 -Wall입니다(표시한 곳은 -Wextra 추가).

nodiscard, deprecated, fallthrough가 g++ -Wall -Wextra에서 만든 경고 출력

그림: 실제 실행 결과(g++ 13.3, Linux) — nodiscard는 반환값을 버린 호출에, deprecated는 사용한 곳에, 암묵적 fallthrough는 -Wextra에서 경고를 내고, 코드는 경고가 있어도 빌드되어 one two를 모두 출력한다. (환경: Linux 6.18.44 · g++ (13.3.0-6ubuntu2~24.04.1))

nodiscard, deprecated

[[nodiscard("반환된 오류 코드를 확인하세요")]] int open_file() { return -1; }
[[nodiscard]] int parse() { return 1; }
[[deprecated("use new_api() instead")]] void old_api() {}

int main() { open_file(); parse(); old_api(); }
warning: ignoring return value of 'int open_file()', declared with attribute 'nodiscard': '반환된 오류 코드를 확인하세요' [-Wunused-result]
warning: ignoring return value of 'int parse()', declared with attribute 'nodiscard' [-Wunused-result]
warning: 'void old_api()' is deprecated: use new_api() instead [-Wdeprecated-declarations]

nodiscard에 문자열을 주면 그 문구가 경고에 그대로 붙어서, 왜 확인해야 하는지가 호출하는 쪽에 전달됩니다. 두 경고 모두 오류가 아니라 경고라서 -Werror 없이는 빌드가 통과합니다.

fallthrough

case 1에서 break 없이 case 2로 흘러내리는 함수입니다. -Wall -Wextra에서 -Wimplicit-fallthrough가 켜집니다.

warning: this statement may fall through [-Wimplicit-fallthrough=]

경고가 나오는 상태로 실행하면 one, two가 모두 출력됩니다. 의도한 fallthrough라면 case 1: 문장 끝에 [[fallthrough]];를 붙이면 같은 옵션에서 경고가 사라지는 것을 확인했습니다. 의도를 코드에 남기는 방법이라, 나중에 읽는 사람이 누락과 구분할 수 있습니다.

noreturn, maybe_unused

[[noreturn]] void die(int c) { if (c) throw 1; }     // c가 0이면 그냥 반환한다

int f(int x, [[maybe_unused]] int y) { return x; }
int g(int x, int y) { return x; }
warning: 'noreturn' function does return
warning: unused parameter 'y' [-Wunused-parameter]        // g에서만, -Wextra

noreturn 함수에 반환 경로가 남아 있으면 컴파일러가 알려 주고, 실제로 반환하면 동작이 정의되지 않습니다. [[maybe_unused]]를 붙인 f의 y는 경고가 없고, 붙이지 않은 g의 y만 경고가 납니다.

컴파일러별 지원

// GCC/Clang
[[gnu::always_inline]]
inline void fastFunc() {}

// MSVC (Visual Studio 2022 이후 버전)
[[msvc::forceinline]]
inline void fastFunc() {}

// 크로스 플랫폼
#ifdef __GNUC__
    [[gnu::always_inline]]
#elif _MSC_VER
    [[msvc::forceinline]]
#endif
inline void fastFunc() {}

벤더 속성은 gnu::, clang::, msvc:: 같은 네임스페이스로 구분됩니다. 앞에서 말했듯 모르는 속성은 무시되므로 #ifdef 없이 둘 다 적어도 빌드는 되지만, 각 컴파일러가 상대의 속성에 “알 수 없는 속성” 경고를 내므로 경고를 깨끗하게 유지하려면 매크로로 감싸는 편이 낫습니다. 특정 속성을 지원하는지는 C++20의 __has_cpp_attribute(nodiscard)로 전처리 단계에서 확인할 수 있고, 값이 표준에 추가된 연월(예: 201603L)로 나와 버전별 기능 차이도 판단할 수 있습니다. 참고로 Clang은 gnu:: 속성 대부분을 이해하므로, __GNUC__ 분기는 GCC와 Clang 모두에 적용됩니다.

자주 발생하는 문제

문제 1: nodiscard 무시

[[nodiscard]] int compute() {
    return 42;
}

int main() {
    (void)compute();  // 명시적 무시 (경고 없음)
    
    compute();  // 경고
}

문제 2: likely/unlikely 오용

// ❌ 잘못된 힌트
if (x == 0) [[likely]] {  // 실제로는 드문 경우
    // ...
}

// ✅ 올바른 힌트
if (x != 0) [[likely]] {  // 실제로 자주 발생
    // ...
}

문제 3: fallthrough 위치

switch (x) {
    case 1:
        cout << "1" << endl;
        [[fallthrough]];  // ✅ case 1의 마지막 문장, 다음 case 레이블 바로 앞
    case 2:
        cout << "2" << endl;
        break;
}
// ❌ [[fallthrough]]; 다음에 다른 문장이 오거나, 마지막 case에 두면 잘못된 코드

들여쓰기는 상관없고, 중요한 것은 [[fallthrough]];가 현재 case에서 마지막으로 실행되는 문장이어야 한다는 점입니다. 그 뒤에 cout 같은 다른 문장이 있으면 “fallthrough 속성이 switch 레이블 바로 앞에 있지 않다”는 진단이 나옵니다. if 블록 안처럼 조건에 따라 fall-through하는 경우에도, 속성 뒤에서 곧바로 다음 case로 흐를 수 있는 위치여야 합니다.

속성 조합

여러 속성은 하나의 대괄호 안에 쉼표로 나열하거나, 대괄호를 따로 써서 붙일 수 있습니다.

[[nodiscard, deprecated("use newFunc")]]
int oldFunc() {
    return 42;
}

[[nodiscard]] [[deprecated]]
int oldFunc2() {
    return 42;
}

실무 패턴

패턴 1: RAII 리소스

class [[nodiscard]] FileHandle {
    FILE* file_;
    
public:
    FileHandle(const char* path) : file_(fopen(path, "r")) {
        if (!file_) {
            throw std::runtime_error("파일 열기 실패");
        }
    }
    
    ~FileHandle() {
        if (file_) {
            fclose(file_);
        }
    }
    
    // 복사 금지
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};

// 사용
// FileHandle("data.txt");  // 경고: 즉시 소멸
auto file = FileHandle("data.txt");  // OK

RAII 타입에서 [[nodiscard]]가 막는 실수는 이름 없는 임시 객체입니다. FileHandle("data.txt");는 파일을 열고 문장이 끝나는 즉시 닫아 버립니다. 이 실수가 가장 위험한 곳은 락입니다. std::lock_guard<std::mutex>{mtx};처럼 변수 이름을 빠뜨리면 컴파일은 되지만 락이 문장 끝에서 바로 풀려, 임계 영역이 전혀 보호되지 않는데도 테스트에서는 대부분 정상으로 보입니다. 괄호로 쓴 std::unique_lock<std::mutex>(mtx);는 더 교묘해서, 임시 객체가 아니라 mtx라는 이름의 새 변수 선언으로 해석되므로 아무것도 잠그지 않습니다. C++20부터 nodiscard 타입의 임시 객체를 만들고 버리는 식도 경고 대상이 되었으므로, 직접 만드는 락이나 스코프 가드 타입에는 클래스 수준 [[nodiscard]]를 붙여 두는 것이 좋습니다.

패턴 2: API 마이그레이션

class Database {
public:
    // 구버전
    [[deprecated("use executeQuery instead")]]
    void query(const std::string& sql) {
        // 구식 구현
    }
    
    // 신버전
    [[nodiscard]] Result executeQuery(const std::string& sql) {
        // 새 구현
        return Result{true, "ok"};
    }
};

// 사용
Database db;
db.query("SELECT * FROM users");  // 경고: deprecated
auto result = db.executeQuery("SELECT * FROM users");  // OK

패턴 3: 성능 최적화

int processData(int* data, size_t size) {
    int sum = 0;
    
    for (size_t i = 0; i < size; ++i) {
        if (data[i] > 0) [[likely]] {
            // 대부분 양수
            sum += data[i];
        } else [[unlikely]] {
            // 드물게 음수
            sum -= data[i];
        }
    }

    return sum;
}

이 패턴은 오히려 역효과가 날 수 있는 대표적인 경우입니다. 이 루프의 분기는 sum += |data[i]|와 같아서, 컴파일러가 속성이 없으면 분기 없는 조건부 이동(cmov)이나 SIMD 명령으로 바꿔 루프 전체를 벡터화할 수 있습니다. 여기에 [[likely]]를 붙이면 컴파일러가 “분기를 유지하고 한쪽을 빠른 경로로 배치하라”는 뜻으로 받아들여, 이런 변환이 막히거나 달라질 수 있습니다. 성능 힌트는 측정 전에는 붙이지 않는 것이 원칙입니다. 속성을 붙이기 전후로 벤치마크를 돌리고, 가능하면 생성된 어셈블리(Compiler Explorer 등)를 확인한 뒤에만 남기는 것이 안전합니다.

표준 속성의 도입 버전

[[...]] 문법 자체는 C++11부터 있지만, 개별 표준 속성은 버전마다 추가되었습니다. 컴파일 옵션의 -std 값이 낮으면 해당 속성은 알 수 없는 속성으로 취급되어 경고만 나고 효과가 없으므로, 프로젝트의 언어 버전과 함께 확인해야 합니다.

표준추가된 속성
C++11[[noreturn]], [[carries_dependency]]
C++14[[deprecated]], [[deprecated("이유")]]
C++17[[fallthrough]], [[nodiscard]], [[maybe_unused]]
C++20[[likely]], [[unlikely]], [[no_unique_address]], [[nodiscard("이유")]]
C++23[[assume(표현식)]]

[[gnu::always_inline]]이나 [[msvc::forceinline]] 같은 네임스페이스가 붙은 속성은 컴파일러 제조사가 정의한 확장이며, 사용자가 새 속성을 정의하는 방법은 표준에 없습니다. 각 속성의 정확한 규칙은 cppreference의 Attributes 문서에서 확인할 수 있습니다.