선언

선언 (Declarations)

C++ 프로그램에 이름과 엔터티를 도입(또는 재도입)하는 방법이 선언(declaration)이에요. 모든 선언이 실제로 무엇인가를 선언하는 건 아니고, 각 종류의 엔터티는 다르게 선언돼요. 정의(definition)는 도입된 엔터티에 대한 완전한 정의를 제공하는 선언이에요.

출처: cppreference

본문

C++ 프로그램에 이름 및/또는 엔터티를 도입(또는 재도입)하는 것이 선언이에요. 모든 선언이 실제로 무엇인가를 선언하는 것은 아니고, 각 종류의 엔터티는 다르게 선언돼요. 정의는 도입된 엔터티에 대한 완전한 정의를 제공하는 선언이에요.

선언은 다음 중 하나예요:

  • 함수 정의
  • 템플릿 선언 (부분 템플릿 특수화 포함)
  • 명시적 템플릿 인스턴스화
  • 명시적 템플릿 특수화
  • 네임스페이스 정의
  • 링키지 사양

| 속성 선언 (attr ;) | (since C++11) |

  • 빈 선언 (;)
  • decl-specifier-seq가 없는 함수 선언:

| attr(선택) declarator ; | | |

항목 - 설명
attr - (since C++11) 임의 개수의 속성 시퀀스
declarator - 함수 선언자

이 선언은 생성자, 소멸자, 또는 사용자 정의 타입 변환 함수를 선언해야 해요. 템플릿 선언, 명시적 특수화, 또는 명시적 인스턴스화의 일부로만 쓸 수 있어요.

  • 블록 선언(block-declaration, 블록 안에 나타날 수 있는 선언)은 차례로 다음 중 하나일 수 있어요:
    • asm 선언

| 타입 별칭 선언 | (since C++11) |

  • 네임스페이스 별칭 정의
  • using-declaration
  • using 지시문

| using-enum-declaration | (since C++20) | | static_assert 선언, 불투명 enum 선언 | (since C++11) |

  • 단순 선언(simple declaration)

C++ 표준은 declaration(구문적 구성)과 declaration(C++ 용어)을 구분한다는 점을 참고해요. 선언은 단순 선언일 뿐만 아니라 클래스 지정자, enum 지정자, 함수 매개변수 등일 수도 있어요. 이름에 대해, 그 이름을 도입하는 선언과 그것이 나타내는 엔터티의 선언이 항상 같은 선언인 것은 아니에요:

template <class T>
struct A {};

이 예에서 이름 A를 도입하는 선언은 클래스 지정자이지만, 클래스 템플릿 엔터티 A를 도입하는 선언은 그것을 둘러싼 템플릿 선언이에요.

단순 선언 (Simple declaration)

단순 선언은 하나 또는 여러 식별자(보통 변수)를 도입·생성·선택적으로 초기화하는 문이에요.

구문 번호
decl-specifier-seq init-declarator-list(선택) ; (1)
attr decl-specifier-seq init-declarator-list(선택) ; (2) (since C++11)
항목 - 설명
decl-specifier-seq - 지정자 시퀀스
init-declarator-list - 쉼표로 구분된 init-declarator 목록 (아래 참고)
attr - 임의 개수의 속성 시퀀스

init-declarator-list는 이름 있는 클래스나 이름 있는 열거형을 선언할 때만 생략할 수 있어요.

| 구조적 바인딩 선언도 단순 선언이다. | (since C++17) |

init-declarator의 구문은 다음과 같이 정의돼요:

구문 번호
declarator initializer (1)
declarator requires-clause(선택) contract-specs(선택) (2)
  1. 초기화식을 가진 선언자.
  2. 초기화식이 없는 선언자.
항목 - 설명
declarator - 선언자
initializer - 초기화식
requires-clause - (since C++20) requires 절
contract-specs - (since C++26) 함수 계약 지정자 목록

| requires-clause는 declarator가 템플릿화된 함수를 선언하는 경우에만 나타날 수 있다. | (since C++20) | | contract-specs는 declarator가 함수 또는 함수 템플릿을 선언하는 경우에만 나타날 수 있다. | (since C++26) |

지정자 (Specifiers)

선언 지정자(decl-specifier-seq)는 다음 공백으로 구분된 지정자들의 시퀀스로, 어떤 순서로든 나타날 수 있어요:

  • typedef 지정자. 있으면 전체 선언은 typedef 선언이고 각 선언자는 객체나 함수가 아니라 새 타입 이름을 도입해요.
  • 함수 지정자(inline, virtual, explicit), 함수 선언에서만 허용됨.

| inline 지정자는 변수 선언에서도 허용된다. | (since C++17) |

  • friend 지정자, 클래스 및 함수 선언에서 허용됨.
  • constexpr 지정자, 변수 정의, 함수 및 함수 템플릿 선언, literal 타입의 정적 데이터 멤버 선언에서만 허용됨. | (since C++11)
  • consteval 지정자, 함수 및 함수 템플릿 선언에서만 허용됨. constinit 지정자, 정적 또는 스레드 저장 기간의 변수 선언에서만 허용됨. constexpr, consteval, constinit 지정자 중 최대 하나만 decl-specifier-seq에 나타날 수 있음. | (since C++20)
  • 저장 클래스 지정자(register, (until C++17)static, thread_local, (since C++11)extern, mutable). 하나의 저장 클래스 지정자만 허용되고, thread_localextern이나 static과 함께 나타날 수 있다는 점은 예외예요(since C++11).
  • 타입 지정자(type-specifier-seq), 타입을 이름 짓는 지정자 시퀀스. 선언이 도입하는 모든 엔터티의 타입은 이 타입이며, 필요하면 선언자가 수정해요 (아래 참고). 이 지정자 시퀀스는 type-id에서도 쓰여요. type-specifier-seq의 일부인 지정자는 다음뿐이며 어떤 순서로든 올 수 있어요:
    • 클래스 지정자
    • enum 지정자
    • 단순 타입 지정자
    • char, char8_t, (since C++20)char16_t, char32_t, (since C++11)wchar_t, bool, short, int, long, signed, unsigned, float, double, void

| auto, decltype 지정자 | (since C++11) | | 팩 인덱싱 지정자, splice 타입 지정자 | (since C++26) |

  • 이전에 선언된 클래스 이름 (선택적으로 한정됨)
  • 이전에 선언된 enum 이름 (선택적으로 한정됨)
  • 이전에 선언된 typedef-name 또는 타입 별칭(since C++11) (선택적으로 한정됨)
  • 템플릿 인자를 가진 템플릿 이름 (선택적으로 한정되고, 선택적으로 template disambiguator 사용)

| 템플릿 인자 없는 템플릿 이름 (선택적으로 한정됨): 클래스 템플릿 인자 추론 참고 | (since C++17) |

  • 확장된 타입 지정자(elaborated type specifier)
  • 클래스 이름으로 이전에 정의된 식별자(선택적으로 한정됨) 뒤에 오는 키워드 class, struct, union
  • 클래스 템플릿 이름으로 이전에 정의된, 템플릿 인자를 가진 템플릿 이름(선택적으로 한정되고 선택적으로 template disambiguator 사용) 뒤에 오는 키워드 class, struct, union
  • 열거형 이름으로 이전에 선언된 식별자(선택적으로 한정됨) 뒤에 오는 키워드 enum
  • typename 지정자
  • cv 한정자

decl-specifier-seq에는 타입 지정자가 하나만 허용되는데, 다음 예외가 있어요:

  • const는 자신을 제외한 어떤 타입 지정자와도 결합될 수 있어요.
  • volatile은 자신을 제외한 어떤 타입 지정자와도 결합될 수 있어요.
  • signed 또는 unsignedchar, long, short, int와 결합될 수 있어요.
  • short 또는 longint와 결합될 수 있어요.
  • longdouble과 결합될 수 있어요.

| longlong과 결합될 수 있다. | (since C++11) |

속성은 decl-specifier-seq에 나타날 수 있고, 그 경우 앞선 지정자들이 결정한 타입에 적용돼요.

decl-specifier-seq에서 어떤 지정자의 반복(예: const static const, virtual inline virtual)은 오류이지만, long이 두 번 나타나는 것은 허용돼요(since C++11).

선언자 (Declarators)

init-declarator-list의 각 init-declarator S D1, D2, D3;는 같은 지정자를 가진 독립적인 선언인 것처럼 처리돼요: S D1; S D2; S D3;.

각 선언자는 정확히 하나의 객체, 참조, 함수, 또는 (typedef 선언의 경우) 타입 별칭을 도입하는데, 그 타입은 decl-specifier-seq가 제공하고 필요하면 선언자의 &(참조), [](배열), ()(함수 반환) 같은 기호가 수정해요. 이 연산자는 아래와 같이 재귀적으로 적용될 수 있어요.

선언자는 다음 중 하나예요:

구문 번호
unqualified-id attr(선택) (1)
qualified-id attr(선택) (2)
... identifier attr(선택) (3) (since C++11)
* attr(선택) cv(선택) declarator (4)
nested-name-specifier * attr(선택) cv(선택) declarator (5)
& attr(선택) declarator (6)
&& attr(선택) declarator (7) (since C++11)
noptr-declarator [ constexpr(선택) ] attr(선택) (8)
noptr-declarator ( parameter-list ) cv(선택) ref(선택) except(선택) attr(선택) (9)
  1. 선언되는 이름.
  2. 한정 식별자(qualified-id)를 사용하는 선언자로, 이전에 선언된 네임스페이스 멤버나 클래스 멤버를 정의하거나 재선언해요. 비friend 선언에 나타나면 이 선언은 네임스페이스 스코프에 있어야 해요.
  3. 매개변수 팩, 매개변수 선언에서만 나타나요.
  4. 포인터 선언자: 선언 S * D;D를 decl-specifier-seq S가 결정한 타입에 대한 포인터로 선언해요.
  5. 멤버에 대한 포인터 선언: 선언 S C::* D;D를 decl-specifier-seq S가 결정한 타입의 C의 멤버에 대한 포인터로 선언해요. nested-name-specifier는 이름과 스코프 확인 연산자 ::의 시퀀스예요.
  6. lvalue 참조 선언자: 선언 S & D;D를 decl-specifier-seq S가 결정한 타입에 대한 lvalue 참조로 선언해요.
  7. rvalue 참조 선언자: 선언 S && D;D를 decl-specifier-seq S가 결정한 타입에 대한 rvalue 참조로 선언해요.
  8. 배열 선언자. noptr-declarator는 유효한 임의 선언자이지만, *, &, &&로 시작하면 괄호로 둘러싸야 해요.
  9. 함수 선언자. noptr-declarator는 유효한 임의 선언자이지만, *, &, &&로 시작하면 괄호로 둘러싸야 해요. 선택적 후행 반환 타입으로 끝날 수 있어요.(since C++11)

| 모든 경우에 attr은 선택적 속성 시퀀스다. 식별자 바로 뒤에 나타나면 선언되는 객체에 적용된다. | (since C++11) |

cvconstvolatile 한정자의 시퀀스인데, 시퀀스에서 각 한정자는 최대 한 번 나타날 수 있어요.

이름 조회 규칙 때문에 타입과 같은 이름을 가진 다른 엔터티의 선언이 같은 스코프에 나타나면 타입 이름이 가려져요. 타입은 확장된 타입 지정자를 통해 여전히 찾을 수 있다는 점을 참고해요.

class Foo {};
void Foo();
// Foo obj;    // Error: 'Foo' is not a type
class Foo obj; // OK

참고 (Notes)

블록 선언이 블록 안에 나타나고, 선언이 도입한 식별자가 바깥 블록에서 이전에 선언되었다면, 바깥 선언은 블록의 나머지 동안 가려져요.

선언이 자동 저장 기간의 변수를 도입하면 그 선언 문이 실행될 때 초기화돼요. 블록에 선언된 모든 자동 변수는 블록에서 나갈 때(예외, goto, 또는 끝 도달 등 블록을 어떻게 나가든) 초기화 순서의 역순으로 파괴돼요.

예제 (Example)

참고: 이 예는 일부 복잡한 선언이 언어 문법 관점에서 어떻게 파싱되는지 보여줘요. 다른 인기 있는 니모닉으로는 나선형 규칙(spiral rule), 안쪽에서 바깥쪽으로 읽기, 선언은 사용을 반영한다(declaration mirrors use) 등이 있어요. https://cdecl.org에 자동 파서도 있어요.

#include <type_traits>

struct S
{
    int member;
    // decl-specifier-seq is "int"
    // declarator is "member"
} obj, *pObj(&obj);
// decl-specifier-seq is "struct S { int member; }"
// declarator "obj" declares an object of type S
// declarator "*pObj" declares a pointer to S,
//     and initializer "(&obj)" initializes it

int i = 1, *p = nullptr, f(), (*pf)(double);
// decl-specifier-seq is "int"
// declarator "i" declares a variable of type int,
//     and initializer "= 1" initializes it
// declarator "*p" declares a variable of type int*,
//     and initializer "= nullptr" initializes it
// declarator "f()" declares (but doesn't define)
//     a function taking no arguments and returning int
// declarator "(*pf)(double)" declares a pointer to function
//     taking double and returning int

int (*(*var1)(double))[3] = nullptr;
// decl-specifier-seq is "int"
// declarator is "(*(*var1)(double))[3]"
// initializer is "= nullptr"

// 1. declarator "(*(*var1)(double))[3]" is an array declarator:
//    Type declared is: "(*(*var1)(double))" array of 3 elements
// 2. declarator "(*(*var1)(double))" is a pointer declarator:
//    Type declared is: "(*var1)(double)" pointer to array of 3 elements
// 3. declarator "(*var1)(double)" is a function declarator:
//    Type declared is: "(*var1)" function taking "(double)",
//    returning pointer to array of 3 elements.
// 4. declarator "(*var1)" is a pointer declarator:
//    Type declared is: "var1" pointer to function taking "(double)",
//    returning pointer to array of 3 elements.
// 5. declarator "var1" is an identifier.
// This declaration declares the object var1 of type "pointer to function
// taking double and returning pointer to array of 3 elements of type int"
// The initializer "= nullptr" provides the initial value of this pointer.

// C++11 alternative syntax:
auto (*var2)(double) -> int (*)[3] = nullptr;
// decl-specifier-seq is "auto"
// declarator is "(*var2)(double) -> int (*)[3]"
// initializer is "= nullptr"

// 1. declarator "(*var2)(double) -> int (*)[3]" is a function declarator:
//    Type declared is: "(*var2)" function taking "(double)", returning "int (*)[3]"
// ...

int main()
{
    static_assert(std::is_same_v<decltype(var1), decltype(var2)>);
}

이 예가 보여주는 핵심은 C++의 선언자를 "안에서 바깥으로" 읽어야 한다는 거예요. int (*(*var1)(double))[3]double을 받고 int 원소 3개의 배열을 가리키는 포인터를 반환하는 함수를 가리키는 포인터예요. C++11부터는 auto (*var2)(double) -> int (*)[3] 같은 후행 반환 타입 구문으로 같은 타입을 더 읽기 쉽게 쓸 수 있어요.

더 알아보기 (Learn more)

  • C 문서의 Declarations.