C++ Attributes | "속성" 완벽 가이드

C++ Attributes | "속성" 완벽 가이드

이 글의 핵심

C++ Attributes에 대한 실전 가이드입니다.

Attributes란?

Attributes (속성) 는 C++11에서 도입된 기능으로, 컴파일러에게 추가 정보를 제공하는 표준화된 방법입니다. 코드 품질 향상, 최적화 힌트, API 문서화 등에 사용됩니다.

왜 필요한가?:

  • 코드 품질: 컴파일러 경고로 버그 방지
  • 최적화: 컴파일러에게 최적화 힌트 제공
  • 문서화: 의도를 명확히 표현
  • 표준화: 컴파일러별 확장 대신 표준 사용
// ❌ 컴파일러별 확장: 비표준
#ifdef __GNUC__
    __attribute__((warn_unused_result))
#endif
int compute();

// ✅ 표준 속성: 모든 컴파일러
[[nodiscard]] int compute();

[[nodiscard]]

반환값을 무시하면 경고를 발생시킵니다. 에러 코드나 중요한 리소스를 반환하는 함수에 사용합니다.

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

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

사용 시나리오:

  • 에러 코드: [[nodiscard]] ErrorCode save()
  • 리소스 핸들: [[nodiscard]] FileHandle open()
  • 중요한 계산: [[nodiscard]] int calculate()
// 에러 코드
[[nodiscard]] bool saveFile(const std::string& path) {
    // 저장 로직
    return true;
}

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

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

[[deprecated]]

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

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

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

[[maybe_unused]]

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

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

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

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

[[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]];  // 경고 없음 (마지막 케이스)
    }
}

[[noreturn]]

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

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

실전 예시

예시 1: 에러 처리

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;
    }
}

예시 2: API 버전 관리

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>

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;
    }
}

예시 4: 스위치 fall-through

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);
    // 초기화
    // 시작
}

컴파일러별 지원

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

// MSVC
[[msvc::forceinline]]
void fastFunc() {}

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

자주 발생하는 문제

문제 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]];  // 여기 아님!
    [[fallthrough]];  // 여기!
    case 2:
        cout << "2" << endl;
        break;
}

속성 조합

[[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

패턴 2: API 마이그레이션

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

// 사용
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;
}

FAQ

Q1: 속성은 언제 사용하나요?

A:

  • 코드 품질: 컴파일러 경고로 버그 방지
  • 최적화: 컴파일러에게 힌트 제공
  • 문서화: 의도를 명확히 표현
  • API 관리: deprecated, nodiscard
[[nodiscard]] bool save();  // 반환값 확인 필수
[[deprecated]] void oldFunc();  // 사용 중단

Q2: nodiscard는 언제 사용하나요?

A:

  • 에러 코드: 반환값 확인 필수
  • 리소스 핸들: RAII 객체
  • 중요한 계산: 결과 무시하면 안됨
[[nodiscard]] ErrorCode connect();
[[nodiscard]] FileHandle open();
[[nodiscard]] int calculate();

Q3: likely/unlikely의 효과는?

A: 분기 예측 최적화로 약간의 성능 향상이 가능합니다.

// 벤치마크 예시
// likely 없이: 100ms
// likely 사용: 95ms (5% 향상)

if (x > 0) [[likely]] {
    // 자주 실행되는 경로
}

Q4: 모든 컴파일러가 지원하나요?

A: 표준 속성은 C++11부터 지원합니다. 일부는 C++17/20에 추가되었습니다.

  • C++11: [[noreturn]], [[carries_dependency]]
  • C++14: [[deprecated]]
  • C++17: [[fallthrough]], [[nodiscard]], [[maybe_unused]]
  • C++20: [[likely]], [[unlikely]], [[no_unique_address]]

Q5: 속성을 무시하면 어떻게 되나요?

A: 컴파일러가 무시합니다. 에러는 아니며, 경고만 발생할 수 있습니다.

[[nodiscard]] int compute();

compute();  // 경고 (무시 가능)

Q6: 여러 속성을 조합할 수 있나요?

A: 가능합니다.

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

// 또는 분리
[[nodiscard]] [[deprecated]]
int oldFunc2() {
    return 42;
}

Q7: 사용자 정의 속성은?

A: 컴파일러별 확장으로 가능하지만, 표준은 아닙니다.

// GCC/Clang
[[gnu::always_inline]] void fastFunc();

// MSVC
[[msvc::forceinline]] void fastFunc();

Q8: 속성 학습 리소스는?

A:

관련 글: nodiscard, deprecated, noreturn.

한 줄 요약: Attributes는 컴파일러에게 추가 정보를 제공하는 C++11 표준화된 방법입니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ noexcept | “예외 없음 지정” 가이드
  • C++ decltype | “타입 추출” 가이드
  • C++ Random | “난수 생성” 가이드

관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |