C++/문법
(♥ 0)
||
C++의 문법을 간략하게 설명하는 문서이다. C언어하고도 중첩되는 요소들이 많으므로 이 문서를 쉽게 이해하기 위해서는 C언어/문법 문서와 비교하여 참조하는 것이 좋다. 하지만 C언어의 문법에 객체지향 문법만 안다고 해서 C++를 잘 아는건 아니다. 일단 C++11 이후로 추가된 기능이 엄청나게 많기 때문이다. 템플릿 공부도 많이 하는 것이 추천되는 편이다.
C++23 이후에 <print> 헤더를 통한 print(), println() 함수를 사용할 수 있게 되었다. 다른 방식으로는 <iostream> 헤더의 cout 객체와, << 연산자를 통한 출력이 유명하다.
일반적으로 C++의 프로그램은 항상 진입점 함수
표준에 따르면
프로그램 종료 시엔 정수로 된 종료 코드를 반환한다. 이 부호는 운영체제마다 다른 의미를 갖고 있는데, 보통
C언어의 자료형을 그대로 사용할 수 있다.
한편 C17까지는 C언어에서 진리값(
C언어에서는 1이 참(True), 0이 거짓(False)로 대응되는 것이 기본적인 사용법이나,
함수는 사실
함수의 정의엔 기본적으로 반환 자료형, 식별자, 소괄호가 필요하며 그리고 선택적으로 매개변수를 추가할 수 있다. 또한 연결성 지시자(
여기서 주의해야할 점은 전달한 인자는 원본이 사용되지 않고, 함수의 지역 변수로 복사돼서 사용된다. 그래서 함수 외부의 변수에 영향을 끼치지 못한다.
C++에서는 사용자가 같은 이름의 함수를 여러개 정의할 수 있다. 함수 오버로딩 혹은 함수 중복 정의 기능은 C언어에서 가장 발전되었다고 볼 수 있는 기능이다. 원래 C언어에서는 모든 함수의 이름이 무조건 달라야 했으나 비슷한 자료형을 인자로 받고 동일한 동작을 수행하는 함수들도 다른 이름으로 구별해야하는 불편함이 있었다. 예를 들어서
한편 오버로딩을 할 때 반환 자료형만 다르게는 만들지 못한다. 함수 오버로딩은 매개변수의 변형을 기준으로 함수를 구분한다. 즉 매개변수가 달라야 중복 정의를 수행할 수 있다.
한편 C++17부터는 이름공간 안의 외부 연결인
구조체는 C언어에서 자료형을 한데 묶기 위해 고안된 복합 자료형이다. 사용자가 직접 제작하고 응용할 수 있는 자료형이다. 구조체를 만들면 기존 원시 자료형처럼 새로운 자료형이 생기는 것이다. 구조체를 만들면 기존의 원시 자료형의 경우와 같이 변수, 함수에 사용할 수 있다. 구조체 변수의 이름 뒤에
구조체를 정의할 때는
"display: none; display: 문단=inline"를 참고하십시오.
다른 자료형 앞에 붙어 값이 불변함을 나타낸다. 가령
만약 포인터와 조합했을때는 포인터가 가리키는 값이 불변인지, 아니면 포인터 자체가 불변인지가 달라진다. 예를 들어
사용되는 예로는 문자열 포인터가 있다. 추후 설명하겠지만 함수의 인자로 전달된
사실 상수 포인터, 상수를 가리키는 포인터는 현재 C++에서는 고려할 필요가 적다. C++17에서
다른 자료형 앞에 붙어 캐시에 대해 최적화를 막고 항상 값이 보이도록 한다. 이를 이해하려면 운영체제와 컴파일러에 대해 이해가 필요하다. 컴파일러는 바이너리를 구축하는 과정에서 인라이닝, 캐싱, 파이프라인 분기 예측, 상수식 평가 등을 동원해 가장 빠른 바이너리를 만든다. 최적화를 하는 과정에서 몇가지 변수와 상수 표현식으로 선언된 상수는 없어진다. 컴파일 시간에 평가할 수 있는 구문은 미리 작성한다. 그리고 평가되지 않을 구문 역시 아예 코드에서 배제된다. 심지어는 메서드의 존재 자체가 없어지고 모든 쓰임새가 반환값으로 대체될 수 있다. 예를 들어 어떤 동일한 for문이 여러 곳에서 반복된다면 for문의 내용을 진짜 반복하는 대신에 결과를 미리 계산해놓고 가져다 쓸 것이다.
이 과정은 거의 대부분의 경우 이득을 가져다 주지만, 변수의 존재 삭제와 구문 단축이 문제가 된다.
참고로
함수의 매개변수인 참조 변수는 함수에 전달된 외부 변수를 그대로 이용할 수 있다. 즉 매개변수로 복사되지 않고 원본을 그대로 이용할 수 있다.
그러나 참조형을 사용할 때 주의점이 있다. 임시 값과 리터럴 값을 이용할 수 없다. C++에서 참조 변수를 쓴다는 것은 변수에 일종의 별명을 붙여주는 것이다. 그런데 임시 값과 리터럴 값은 어떤 변수에 저장하기 전까진 메모리 주소도 없으며 수정할 수 없는 값들이다. 가령 예제의
그렇지만 예제의
C++11에서 새로 도입된 인자 전달 방식이다.
한편
사실
기존에는 한정자 없는 생성자 또는
또한 한번 사용되면 바로 사라질 값이기에 중복해서 사용할 수 없다. 예를 들어 가변 배열인 표준 라이브러리의
"display: none; display: 문단=inline"를 참고하십시오.
"display: none; display: 문단=inline"를 참고하십시오.
예제에서는 "
이 역시 컴파일러의 구현마다 다르지만 보통 동적 할당은 스택대신 힙(Heap) 영역에 이루어진다. C++에서는
유의할 점은 C언어/문법#동적 할당에서 소개하는
결론적으로 C++의 객체를 C언어 방식으로 할당하면 생성자, 소멸자가 호출되지 않으므로, 정말 특수한 상황이 아니면
new는 메모리를 할당하는데 실패할 경우 std::bad_alloc 예외를 던진다. 예외 발생으로 인한 성능 저하를 걱정한다면 set_new_handler 함수를 통해 할당 실패 시 예외 대신 nullptr 값을 반환하도록 만들 수 있다.
반대로 이미 해제한 메모리를 재차 해제하려 하거나(Double-Free), 이미 해제한 메모리에 접근하려 할 경우(Use-After-Free)에도 치명적인 문제가 발생한다. 마지막으로
마지막으로 백엔드에서 서로 다른 런타임을 사용할 때는 경우 해제(
"display: none; display: 문단=inline"를 참고하십시오.
Storage Duration
정적인 기억공간 (
주의해야할 점은
클래스의 정적인 멤버는 외부 연결로 취급된다. 이 규칙이 없으면 헤더와 모듈C++20 에서 정적 멤버를 사용할 수 없을 것이다. 정적 멤버의 종류는
클래스 내의 정적 멤버는 클래스 인스턴스를 만들 필요도 없이 클래스 이름만으로 사용할 수 있다. C++에는 메타클래스가 없으므로 클래스 자체는 메모리에 정적으로 고정된 객체다. 그리고 클래스의 고정된 메모리 주소로부터 상대적 위치만 저장해서 멤버를 구별한다. 그래서 C++의 클래스는 정적 멤버가 아니더라도 일단
정적인 외부 연결 (
객체 선언의 자료형 앞에
두번째 기능도 있는데, 템플릿 실체화(Template Instantiation) 기능이다. 템플릿의 구현을 해당 소스에서만 컴파일하도록 지시한다. 템플릿은 모든 자료형 후보에 대해 컴파일을 시도하는데, 이러면 컴파일 시간이 기하급수적으로 늘어날 수 있다. 이때 원하는 특수화 후보를 명시하면 중복되는 컴파일 시도를 줄일 수 있다.
세번째로 C와 C++를 언어 전환하는 기능도 있다.
해당 키워드를 가장 많이 쓰는 경우는 ABI가 호환되는 라이브러리를 만들 때로, 예를 들어
이름공간[11] , 클래스에서 변수 선언에 사용할 수 있다. 전역 변수 선언에 사용하면 이 순간부터 프로그램에서 사용한 모든 스레드에 해당 변수가 선언되게 된다. 참고로 내외부 연결성에 관여하지는 않으므로,
클래스의 필드에 사용할 수 있다.
C++11 부터 도입된 상수 표현식, 또는 상수식은 실제 코드가 실행되는 사용자 시점(런타임)이 아니라 컴파일 시점으로 코드의 평가를 앞당길 수 있는 획기적인 기능이다. C++의 킬러 요소라고 말할 수 있는 핵심 기능이다 [12] . 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정되기 때문에 아무런 평가 과정도 컴파일 이후에 남지 않는다. 곧 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다.
"display: none; display: 문단=inline"를 참고하십시오.
Requires Clause (requires 절)
"display: none; display: 문단=inline"를 참고하십시오.
자료형, 함수, 클래스를 모두 읽고 오는 것을 추천한다.
주의할 점은 구조체의 멤버 변수, 함수의 선언 시에는 사용이 불가능하다는 것이다.
지금까지 본 함수 예제에서 전달한 인자들이 복사된다는 사실을 알았다.
이것이 유용한 예로는 정수와 큰 정수의 연산, 실수와 정수 사이의 연산, 비용이 큰 연산을 하기 전에 자료형을 가져와서 미리 준비를 할때다. 서로 다른 정수 사이에는 더 바이트 수가 큰 정수로 승급하는 규칙이 있고, 실수와 정수 사이에는 실수로 변환되는 규칙이 있다. 비용이 큰 연산에는 직렬화, 문자열 포맷 등이 있다. 직렬화를 하려면 원본 자료형을 더 작은 자료형으로 변환하거나, 일정한 크기의 메모리에 변환된 값들을 쓰는 과정이 필요하다. 이때 실제로 변환 작업을 하기 전에 먼저 필요한 메모리를 할당하는 수가 있다. 표준 라이브러리에서 사용하는 예로, 시간 라이브러리
만약
그래서 사용자가
"display: none; display: 문단=inline"를 참고하십시오.
"display: none; display: 문단=inline"를 참고하십시오.
1. 개요
2. Hello, world!
3. 프로그램 진입점
4. 원시 자료형 (Primitives)
4.1. Truthy / Falsy
5. 함수 (Function)
5.1. 반환 자료형 (Return Type)
5.1.1. return
5.2. 매개변수 (Parameter)
5.3. 오버로딩 (Overloading)
5.4. inline
5.5. noexcept
6. 구조체 (Struct)
8. 자료형 한정자 (Type Qualifier)
11. 메모리 할당
12. 언어 연결성 (Language Linkage)
13. 저장소 지속요건 (Storage Duration)
15. 제약조건
16. 값 범주론 (Value Category)
17. 템플릿 (Template)
18. 개념 (Concept)
1. 개요[편집]
C++의 문법을 간략하게 설명하는 문서이다. C언어하고도 중첩되는 요소들이 많으므로 이 문서를 쉽게 이해하기 위해서는 C언어/문법 문서와 비교하여 참조하는 것이 좋다. 하지만 C언어의 문법에 객체지향 문법만 안다고 해서 C++를 잘 아는건 아니다. 일단 C++11 이후로 추가된 기능이 엄청나게 많기 때문이다. 템플릿 공부도 많이 하는 것이 추천되는 편이다.
###include <iostream>
##int main()
##{
## std::cout << "Hello, world!\n";
## return 0;
##}
##
2. Hello, world![편집]
import <print>;
int main()
{
std::println("Hello, world!");
std::println("{}, {} {}!", "Hello", "world", "again");
}
C++23 이후에 <print> 헤더를 통한 print(), println() 함수를 사용할 수 있게 되었다. 다른 방식으로는 <iostream> 헤더의 cout 객체와, << 연산자를 통한 출력이 유명하다.
3. 프로그램 진입점[편집]
main
함수일반적으로 C++의 프로그램은 항상 진입점 함수
main
을 요구한다. main
함수는 C++ 프로그램의 시작과 끝을 담당한다. 사용자가 main
함수의 중괄호 안에 코드를 작성하고 컴파일하면 해당 코드를 실행한다.표준에 따르면
main
함수를 작성하는 방식은 두 가지가 있다. 첫번째는 아무런 매개변수가 없이 소괄호만 있는 경우다. 대부분의 경우 이걸로 충분하다. 두번째는 외부에서 문자열로 된 매개변수가 전달된 main
함수다. 많은 운영체제에서는 프로그램이 실행될 때 외부에서 매개변수를 전달할 수 있다. 가령 Windows에서는 프로그램의 바로가기를 만들고 프로그램 경로 뒤에 -path "C:\\Program Files\\Microsoft"
처럼 매개변수를 전달할 수 있다.프로그램 종료 시엔 정수로 된 종료 코드를 반환한다. 이 부호는 운영체제마다 다른 의미를 갖고 있는데, 보통
0
을 반환하면 main
함수의 실행에 오류가 없이 성공적으로 종료되었다는 뜻이다. 하지만 return 0;
은 반드시 쓰지 않아도 된다. 아예 구문이 없어도 컴파일러가 알아서 처리해준다.4. 원시 자료형 (Primitives)[편집]
C언어의 자료형을 그대로 사용할 수 있다.
한편 C17까지는 C언어에서 진리값(
bool
[1] )을 사용하려면 <stdbool.h>
를 삽입하고 _Bool
, 0, 1을 써야 했으나 C23에서 bool
, true
, false
가 정식으로 편입되면서 원시 자료형에서는 C언어와 C++의 차이가 아예 사라졌다. C에서 파생 자료형으로 제공되던 size_t
, rsize_t
, ptrdiff_t
도 제공한다.4.1. Truthy / Falsy[편집]
C언어에서는 1이 참(True), 0이 거짓(False)로 대응되는 것이 기본적인 사용법이나,
0
이 아닌 숫자는 모두 참으로 간주돼었다. C++에서는 진리값(bool
) 자료형이 추가되면서 true
, false
를 이용할 수 있다.5. 함수 (Function)[편집]
함수 (Function)
함수는 사실
main
함수 말고도 사용자가 직접 만들 수 있다. 여기서 말하는 함수는 C++에서 실행할 수 있는 코드의 집합이자 하나의 문맥 분기점이다. 이렇게 작성해 두었다가 함수의 이름과 함께 ()
[호출] 를 붙여 실행할 수 있다. 함수가 실행되면 결과 값을 반환하고 원래 호출 스택으로 되돌아온다. 앞서 살펴본 main
함수는 사용자가 이용할 수 있는 함수 중의 하나다. main
함수는 프로그램 안에서 유일해야 한다는 제약만 빼면 마찬가지로 정수를 반환하는 보통의 함수와 다를 것이 없다. C++ 개발자는 함수를 통해 코드를 쉽게 정리하고 재사용할 수 있다. 궁극적으로 더 효율적이고 유지보수가 용이한 프로그램을 만들 수 있다.함수의 정의엔 기본적으로 반환 자료형, 식별자, 소괄호가 필요하며 그리고 선택적으로 매개변수를 추가할 수 있다. 또한 연결성 지시자(
static
, extern
), 예외 사양(nothrow
/noexcept
C++11 ), 평가 지시자(constexpr
C++11 , consteval
C++20 ), 혹은 특성C++11 을 부착할 수도 있다.5.1. 반환 자료형 (Return Type)[편집]
반환 자료형은 함수의 실행 결과로 어떤 종류의 값이 나오는지 나타내는 자료형이다. 함수를 정의할 때는 식별자 앞에 반환 자료형을 넣어야 한다. 앞서 살펴본
main
함수는 32비트 int
를 반환하는 규칙이 있으므로 int
를 함수의 이름 앞에 기입해야 했다. 만약 함수가 아무것도 반환하지 않고, 어떤 코드만 실행한다면 void
를 기입하면 된다.5.1.1. return[편집]
return
은 함수를 종료하고 이전 스택으로 되돌아가거나, 값을 반환하는 명령어다. main
함수에는 return
0;
을 쓰지 않아도 된다는 특별한 규칙이 있다. 하지만 이 규칙은 사용자가 만든 함수에는 적용되지 않는다. 사용자는 반환 자료형만 기입해선 안되고 함수 내부에서 return
반환값;
구문을 실행해줘야 한다.아무것도 반환하지 않는
void
함수라면 실행할 필요는 없다. 만약 void
함수에서 return
;
을 실행하면 함수가 즉시 종료된다. 특이한 점은 아무것도 반환하지 않는 함수에서 어떤 아무것도 반환하지 않는 함수 실행 구문을 return
하는 건 문제가 되지 않는다. 해당 함수가 실행되고나서 함수가 즉시 종료된다.5.2. 매개변수 (Parameter)[편집]
C++에는 함수 내부로 값을 전달하는 기능이 있다. 이렇게 함수 외부에서 전달된 값이 들어오는 변수를 매개변수라고 한다. 함수의 지역 변수(Local Variable)로 사용하기 위해 인자를 전달하는 것이다. 함수를 호출할 때 소괄호 안에 전달할 인자를 기입하면 된다. 이렇게 전달한 인자는 함수의 매개 변수에 값이 들어온다.
여기서 주의해야할 점은 전달한 인자는 원본이 사용되지 않고, 함수의 지역 변수로 복사돼서 사용된다. 그래서 함수 외부의 변수에 영향을 끼치지 못한다.
<C++ 예제 보기> import <print>; void increment1(int x) { ++x; } void increment2(int x) { ++x; } void increment3(int x, int y) { x += y; } int main() { int a = 100; int b = 500; // (1) increment1(a); // (2) increment1(7124820); // (3) increment2(a); // (4) increment2(b); // (5) increment3(a, 9058142); // (6) increment3(a, b); std::println("a의 값: {}", a) // 100을 출력함 std::println("b의 값: {}", b) // 500을 출력함 }
increment1
, increment2
, increment3
를 통해 변수 a
와 b
를 조작하려고 시도하는 예제다. 하지만 a
와 b
의 값은 변하지 않는다. 함수 increment1
, increment2
, increment3
에 전달된 a
와 b
는 각각 함수의 매개변수에 값만 전달되었기 때문이다. 함수 안에서는 매개변수인 x
와 y
를 수정하기 때문에 원래 변수 a
, b
에는 아무런 영향을 주지 못한다.5.3. 오버로딩 (Overloading)[편집]
함수 과다 적재 (Function Overloading)
C++에서는 사용자가 같은 이름의 함수를 여러개 정의할 수 있다. 함수 오버로딩 혹은 함수 중복 정의 기능은 C언어에서 가장 발전되었다고 볼 수 있는 기능이다. 원래 C언어에서는 모든 함수의 이름이 무조건 달라야 했으나 비슷한 자료형을 인자로 받고 동일한 동작을 수행하는 함수들도 다른 이름으로 구별해야하는 불편함이 있었다. 예를 들어서
int
또는 long long
을 받아 문자열로 바꾸는 함수가 있다면, 그 함수의 이름은 ConvertIntToString()
, ConvertLongLongToString()
따위를 사용해야 했었다. 또한 이름공간의 부재로 인한 식별자 부족 현상도 있었다. C++에서는 ConvertToString()
처럼 같은 양식의 식별자로 통일하고 어디에서나 일관적인 코드 작성이 가능해졌다.한편 오버로딩을 할 때 반환 자료형만 다르게는 만들지 못한다. 함수 오버로딩은 매개변수의 변형을 기준으로 함수를 구분한다. 즉 매개변수가 달라야 중복 정의를 수행할 수 있다.
5.4. inline[편집]
inline
함수inline
을 반환 자료형 앞에 붙이면 해당 함수의 쓰임새 부분이 함수의 코드 자체로 대체될 수 있음을 나타낸다 [2] . 이 키워드가 적절하게 쓰이면 함수 호출 오버헤드를 줄이고, 호출 스택도 아낄 수 있어서 좋다. 심지어 최적화 과정에서 아예 함수의 코드를 날리고 결과값만 남길 수도 있다. 그렇지만 대부분의 현대 컴파일러는 알아서 처리를 해준다. 그래서 이 지시자의 의의는 코드를 읽는 다른 개발자들에게 이 함수가 인라이닝이 되도록 설계되었다는 것을 알리면서 컴파일러에게 더 적극적으로 인라이닝을 하라는 지시에 가깝다.inline
함수는 정의와 선언이 같이 행해져야 한다. 정의가 없는 inline
함수는 컴파일 오류를 발생시킨다. 한편 C++17부터는 이름공간 안의 외부 연결인
inline
함수[3] 는 규칙이 생겼다. 컴파일러의 해석 단위(Translation Unit)[헤더] 에서 항상 같은 이름과, 같은 명세를 가지게 정의해야 하도록 되었다. 즉 기존의 static
함수와 같은 규칙을 가지게 되었다. 다시 말해서 여러 헤더에서 참조하는 동일한 동일한 이름의 inline
함수는 매개변수나 noexcept
등의 정의가 달라지면 안된다는 뜻이다. 이런 규칙이 제정된 이유는 헤더를 삽입했는데 같은 서명을 가진 함수가 다른 객체로 인식되는 문제가 있기 때문이다. 같은 헤더의 같은 이름공간에서 같은 이름인 함수를 가져왔는데 어떻게 다른 함수일 수 있겠냐는 것이다. 그리고 이로써 inline
함수는 정적 함수는 아니지만, 프로그램 내에서는 똑같은 이름은 곧 동일한 함수며 따라서 항상 같은 메모리 위치에 존재함이 보장되었다.5.5. noexcept[편집]
함수에서 예외를 던지는지 여부를
noexcept
를 통해 지정할 수 있다. 이를 통해 컴파일러에게 예외 검사를 배제하도록 지시할 수 있다. 예외를 던질지 말지는 사용자의 자유이지만, 확실하게 오류가 없는 함수라면 noexcept
를 놓으면 된다. C++20부터는 noexcept(상수 진리값)
를 통해 선택적으로 예외 여부를 지정할 수도 있다. 이를 위해 표준 라이브러리에서는 <type_traits>
모듈에서 std::is_nothrow_*
같은 명칭의 메타 함수를 제공하고 있다. noexcept
안의 표현식은 묵시적으로 평가되기 때문에 복잡한 코드가 달려있다고 성능에 문제는 생기지 않는다.6. 구조체 (Struct)[편집]
구조체 (Struct)
구조체는 C언어에서 자료형을 한데 묶기 위해 고안된 복합 자료형이다. 사용자가 직접 제작하고 응용할 수 있는 자료형이다. 구조체를 만들면 기존 원시 자료형처럼 새로운 자료형이 생기는 것이다. 구조체를 만들면 기존의 원시 자료형의 경우와 같이 변수, 함수에 사용할 수 있다. 구조체 변수의 이름 뒤에
.
를 붙이면 구조체의 멤버에 접근할 수 있다.구조체를 정의할 때는
struct
키워드 뒤에 구조체의 식별자를 붙이면 된다. struct
키워드와 식별자 사이에 특성C++11 을 부착할 수도 있다.<C++ 예제 보기> import <filesystem>; struct [[nodiscard]] Sprite { std::filesystem::path imagePath; int imageIndex; }; struct Player { Sprite mySprite; float myHealth; float myPosition[2]; }; Player player_instance; void PlayerMoveX(float dist) { player_instance.myPosition[0] += dist; } void PlayerMoveY(float dist) { player_instance.myPosition[1] += dist; } [[nodiscard]] float PlayerGetHealth() { return player_instance.myHealth; } [[nodiscard]] std::filesystem::path SpriteGetImagePath(Sprite sprite) { return sprite.imagePath; } int main() { Sprite player_sprite { .imagePath = L"/res/img/Player/Idle.png", .imageIndex = 0 }; player_instance = Player{ player_sprite, 100.0f, 0, 0 }; }
7. 자료형 (Data Types)[편집]
"display: none; display: 문단=inline"를 참고하십시오.
8. 자료형 한정자 (Type Qualifier)[편집]
8.1. const[편집]
상수 (Constant)
다른 자료형 앞에 붙어 값이 불변함을 나타낸다. 가령
const int
는 변하지 않는 정수 값임을 의미한다. 사용자 단에서는 변수 선언 시, 함수의 결과값을 const
로 표시하여 그 값을 변경하지 말라는 지시 효과를 볼 수 있다. 함수 구현 시에는 매개 변수에 붙여 사용자의 실수를 줄이고 예측할 수 없는 값의 수정을 막아준다.만약 포인터와 조합했을때는 포인터가 가리키는 값이 불변인지, 아니면 포인터 자체가 불변인지가 달라진다. 예를 들어
const int* ptr;
은 const int
의 포인터이므로, *ptr
로 접근한 값은 const
라서 수정할 수 없다. 그러나 ptr
이라는 변수 자체는 더하고 빼고 등등 임의의 연산도 할 수 있다. int* const ptr;
은 ptr
자체가 불변이지만 *ptr
로 접근한 값은 그냥 int
라서 바꿀 수 있다.사용되는 예로는 문자열 포인터가 있다. 추후 설명하겠지만 함수의 인자로 전달된
const char* string
은 C++에서 문자열을 의미한다. 그런데 포인터 자체가 아니라 문자열의 값이 불변이라서 변수 string
에 1을 더하고 빼는 등 값을 변경할 수 있다. 표준 라이브러리의 <algorithm>
모듈에서는 이를 이용해 자료구조 뿐만 아니라 문자열에 대해서도 동일한 연산을 지원한다. 반면 char* const& character
는 character
가 가리키는 char*
값 하나는 *character = 'B';
처럼 언제든지 값이 바뀔 수 있다. 그러나 character
는 불변이다.사실 상수 포인터, 상수를 가리키는 포인터는 현재 C++에서는 고려할 필요가 적다. C++17에서
std::string_view
, C++20에서 std::span
의 도입으로 웬만한 포인터 사용을 대체할 수 있기 때문이다 [4] . 이해하기 어렵다면 다른 자료형 앞에 붙어야 의미가 있다는 것을 기억하자.8.2. volatile[편집]
휘발성 (Volatile)
다른 자료형 앞에 붙어 캐시에 대해 최적화를 막고 항상 값이 보이도록 한다. 이를 이해하려면 운영체제와 컴파일러에 대해 이해가 필요하다. 컴파일러는 바이너리를 구축하는 과정에서 인라이닝, 캐싱, 파이프라인 분기 예측, 상수식 평가 등을 동원해 가장 빠른 바이너리를 만든다. 최적화를 하는 과정에서 몇가지 변수와 상수 표현식으로 선언된 상수는 없어진다. 컴파일 시간에 평가할 수 있는 구문은 미리 작성한다. 그리고 평가되지 않을 구문 역시 아예 코드에서 배제된다. 심지어는 메서드의 존재 자체가 없어지고 모든 쓰임새가 반환값으로 대체될 수 있다. 예를 들어 어떤 동일한 for문이 여러 곳에서 반복된다면 for문의 내용을 진짜 반복하는 대신에 결과를 미리 계산해놓고 가져다 쓸 것이다.
이 과정은 거의 대부분의 경우 이득을 가져다 주지만, 변수의 존재 삭제와 구문 단축이 문제가 된다.
while(bool 변수);
로 문맥의 흐름을 막았다고 해보자. 그럼 while
문 안의 bool
변수가 다른 스레드에서 false
로 바뀌면 무한 루프를 빠져나갈까? 정답은 그렇지 않다. 계속 갇혀있다. 왜냐하면 while
안의 변수는 최적화를 위해 메모리를 읽지 않고 컴파일 순간 정해진 캐시의 값을 계속 가져오기 때문이다. 이를 다른 스레드에서 보이지 않는 값이 되었다고 한다. 또다른 예로는 프로그램 내내 같은 주소를 가리키는 포인터의 값을 읽을 필요가 있다고 해보자. 이러면 컴파일러는 컴파일 당시에 해당 포인터가 가리켰던 값만을 가져온다. 그래서 해당 포인터가 가리키는 값이 변경되어도 프로그램에선 바뀐 값을 알지 못한다. 바로 이럴때 volatile
을 붙여서 캐시말고 항상 메모리에서 가져오도록 만들 수 있다. 즉 휘발성이라는 말은 현재 변수에서 읽어올 값이 임시적이라는 의미다.참고로
volatile
은 컴파일러의 캐시 최적화, 인위적인 코드 패치 순서 수정[5] 을 막을 뿐이므로 다중 스레드를 사용할 때는 여전히 값을 읽기 순서에 따른 동기화 문제가 발생할 수 있다.8.3. & 참조[편집]
좌측값 참조자 (Left-Value Reference)
Type& identifier = value;
와 같이 &
를 다른 자료형 뒤에 붙여 값이 참조 형태임을 나타낸다. 가령 int&
는 다른 정수를 참조하는 변수임을 의미한다. 참조 변수에 값을 할당하면 원래 변수에 값이 전달된다. 구조체 참조 변수의 경우 .
로 멤버에 접근하면 원래 구조체의 속성에 접근할 수 있다. 다시 말하면 참조 변수는 원래 변수에 또다른 이름을 붙여주었다고 말할 수 있다. 만약 &
연산자를 변수 앞에 붙여 주소를 얻으면 참조 변수의 주소가 아니라, 가리키는 변수의 주소가 나온다. 주소를 가져오는 표준 라이브러리의 std::addressof
함수를 사용해도 마찬가지다.<C++ 예제 보기> int original_v1 = 1'000'000; int original_v2 = 7'000'000; int original_v3 = 9'000'000; int& ref_v1 = original_v1; int& ref_v2 = original_v2; int& ref_v3 = original_v3; ref_v1 = 1000; // original_v1의 값이 1000이 된다. int* handle_v1 = &ref_v1; // original_v1의 주소를 가져온다. ref_v1 = original_v2; // 참조 변수 ref_v1을 통해 original_v1의 값이 7000000이 된다. ref_v1 = ref_v2; // 참조 변수 ref_v2를 통해 original_v2의 값을 가져오고, ref_v1을 통해 original_v1의 값이 7000000이 된다. *handle_v1 = original_v3;// handle_v1의 주소가 바뀌지 않는다. original_v1의 값이 9000000이 된다.
original_v1
, original_v2
, original_v3
에 대해 각각의 참조 변수를 만들고 접근하는 예제다. 각각 ref_v1
, ref_v2
, ref_v3
로 참조되어 이 참조 변수를 통해 원래 변수를 수정할 수 있다. 그렇다면 이름만 바꿔 부르는 이 단순한 기능을 어디에 쓰는 걸까? 참조 변수는 함수에서 더 잘 활용된다. 앞서 소개한 구조체 예제를 다시 살펴보자.<C++ 예제 보기> // 생략 Player player_instance; void PlayerMoveX(float dist) { player_instance.myPosition[0] += dist; } void PlayerMoveY(float dist) { player_instance.myPosition[1] += dist; } [[nodiscard]] float PlayerGetHealth() { return player_instance.myHealth; }
PlayerMoveX
, PlayerMoveY
를 쓰는데 문제가 없었다. 그러나 플레이어 개체가 여러개라면 함수에서 참조하는 플레이어 개체 변수가 하나 뿐이라 이 방법은 쓰지 못한다. 플레이어가 아니더라도 게임에서 다수가 등장하는 적의 경우 모든 개체에 대해 함수를 만들지 않는 이상 이 방법을 쓰지 못한다. 그렇다면 이 함수를 모든 플레이어에 대해 포괄적으로 적용되게 만들어야 한다. 그렇다면 어떻게 할까? 참고로 플레이어를 매개변수로 전달하면 플레이어 변수가 복사되므로 사용할 수 없다. 이럴 때는 함수의 매개변수를 참조 변수로 만들어야 한다.8.3.1. 함수에 좌측값 참조로 전달[편집]
좌측값 참조로 전달 (Pass by lvalue reference)
함수의 매개변수인 참조 변수는 함수에 전달된 외부 변수를 그대로 이용할 수 있다. 즉 매개변수로 복사되지 않고 원본을 그대로 이용할 수 있다.
<C++ 예제 보기> // 생략 Player playerInstances[4]; int localPlayerIndex = 0; void PlayerMoveX(Player& player, float dist) { player.myPosition[0] += dist; } void PlayerMoveY(Player& player, float dist) { player.myPosition[1] += dist; } [[nodiscard]] float PlayerGetHealth(Player& player) { return player.myHealth; } int main() { Sprite player_sprite { .imagePath = L"/res/img/Player/Idle.png", .imageIndex = 0 }; playerInstances[localPlayerIndex] = Player{ player_sprite, 100.0f, 20, 180 }; playerInstances[localPlayerIndex + 1] = Player{ player_sprite, 100.0f, 180, 180 }; playerInstances[localPlayerIndex + 2] = Player{ player_sprite, 100.0f, 20, 20 }; playerInstances[localPlayerIndex + 3] = Player{ player_sprite, 100.0f, 180, 20 }; PlayerMoveY(playerInstances[localPlayerIndex + 2], 30); // 오류! PlayerMoveX(Player{ player_sprite, 100.0f, 100, 100 }, 50); }
Player
의 참조형을 받고, 받은 개체의 멤버 변수에 접근한다.그러나 참조형을 사용할 때 주의점이 있다. 임시 값과 리터럴 값을 이용할 수 없다. C++에서 참조 변수를 쓴다는 것은 변수에 일종의 별명을 붙여주는 것이다. 그런데 임시 값과 리터럴 값은 어떤 변수에 저장하기 전까진 메모리 주소도 없으며 수정할 수 없는 값들이다. 가령 예제의
Player{ player_sprite, 100.0f, 100, 100
}는 Player
구조체의 임시 값이다. 그리고 100.0f
같은 순수한 숫자값은 이름이 없는 리터럴 값이다. 때문에 const
가 아닌 좌측 참조 변수는 임시 값 및 리터럴 값을 받을 수 없다.그렇지만 예제의
PlayerMoveX
, PlayerMoveY
는 원래 수정되어야 할 플레이어 객체를 사용해야 하므로 임시 값을 받는 게 오히려 문제가 된다. 이런 식으로 원본이 필요한 곳에는 &
를, 아닌 곳에는 일반 자료형을 쓰면 문제가 없는 것 처럼 보인다. 겁을 주는 것 같지만 사실 거의 대다수의 경우 이 정도의 활용으로 충분하다.8.4. && 참조[편집]
우측값 참조자 (Right-Value Reference)C++11
C++11에서 새로 도입된 인자 전달 방식이다.
&&
는 변수에 저장되기 전에는 메모리에도 존재하지 않고 이름이 없는 임시 객체를 혹은 리터럴을 표현하기 위한 한정자다. 원래 이 값들은 전통적으로 C언어에서 rvalue
(Right-Value, 우측값)라고 칭하는 값이다. 왜냐하면 임시 값이 선언, 대입, 비교문에서 식의 오른쪽에 놓이는 경향이 있어서 이렇게 지칭되어 왔다 [6] . rvalue
는 우측값 참조자를 통해 변수가 사용되는 메모리 공간에 복사없이, 마치 처음부터 존재하는 것처럼 처리할 수 있다. 복사가 아예 일어나지 않기에 값의 교환을 효율적으로 할 수 있다. 그래서 &&
를 다른 말로 이동 연산자라고도 부른다.rvalue
의 종류에는 구조체/클래스의 생성자를 호출해서 만든 임시 객체, 숫자 리터럴, 문자 리터럴, &
참조가 아닌 함수의 반환 값 등이 있다. 이 값들을 변수에 넣지 말고 바로 사용할 때 rvalue
다.한편
&&
를 C++에서 실제로 활용하려면 사실상 함수의 사용이 필수적이다. 왜냐하면 변수에서 쓰이는 &&
참조형은 큰 의미가 없기 때문이다. 그 이유는 &&
의 부스러지는 특징 때문이다. &&
는 &
참조형처럼 여전히 스스로 존재할 수 없는 존재다. 그런데 &&
는 변수의 이름 자체도 아무런 의미가 없다. 이게 무슨 뜻이냐 하면, C++에서 변수의 이름은 좌측값(lvalue
)인데 &&
참조 변수의 이름을 언급하는 순간 그건 rvalue
가 아니게 되는 것이다! 때문에 반드시 std::move
, static_cast<T&&>
따위로 감싸줘야 한다. 사용자가 직접 &&
자료형을 명시하던가, 아니면 함수의 도움 없이는 &&
는 항상 &
로, const&&
는 항상 const&
로 연역된다 [7] . C++의 표준 라이브러리에서는 <utility>
모듈의 std::move
라는 함수로 간편한 이동 연산을 제공한다. 또는 사용자가 직접 static_cast<T&&>(value)
로 지시할 수 있다.사실
&&
는 입문 시기에는 직접 쓸 필요가 없다. 컴파일러가 알아서 복사, 이동 생성자를 만들어주니까. 사실 운영체제 기능을 쓰지 않는다면 그렇게 필수적인 기능이 아니다. 하지만 성능을 위해 학습 진도를 조금만 넘겨도 혜성처럼 등장하고, 고급 단계에서 이해하지 못하면 C++의 알 수 없는 기전에 좌절할 수 있다. 8.4.1. 함수에 우측값 참조로 전달[편집]
우측값 참조로 전달 (Pass by rvalue reference)C++11
<C++ 예제 보기> import <string>; struct Position { float x, y, z; }; Position MakePosition_Copy(const Position& pos) { return Position{ pos }; } Position MakePosition_Move(Position&& pos) { // 매개변수 pos를 그대로 전이 return std::move(pos); // 또는 이동 생성자 사용 return Position{ std::move(pos) }; // 또는 수동으로 rvalue 변환 return Position{ static_cast<Position&&>(pos) }; } struct Squirrel { std::string myName; Position myPosition; }; void SetPosition_Copy(Squirrel& squirrel, const Position& pos) { squirrel.myPosition = pos; } void SetPosition_Move(Squirrel& squirrel, Position&& pos) { squirrel.myPosition = std::move(pos); // 또는 수동으로 rvalue 변환 squirrel.myPosition = static_cast<Position&&>(pos); } void Function3_pos_copy(Squirrel& squirrel, const float& x, const float& y, const float& z); void Function3_pos_move(Squirrel& squirrel, float&& x, float&& y, float&& z);
&
참조자 역시 최대한 얕은 복사로의 유도를 했을 뿐, 어쩔 수 없이 임시 객체 생성, 변수의 중복 선언 등으로 인한 오버헤드가 있었다. 시스템 자원의 중복도 큰 문제가 되었다. 가령 스레드, 뮤텍스, GDI 객체, 핸들은 시스템에서 생성되고 관리된다. 사용자는 운영체제 호출을 통해 간접적으로 제어할 수 있다. 그런데 생성, 제어는 그렇다 치고 이 중복된 자원들이 파괴되는 경우가 있을 것이다. 이때 다른 곳에서 핸들이 파괴된 일을 모르면 잘못된 운영체제 호출이 발생하고, 이 오류는 단순한 런타임 오류와는 궤를 달리할 것이다.기존에는 한정자 없는 생성자 또는
const&
생성자 뿐이었고, 이를 본질적으로 구분할 수 없다는 문제가 있었다. 생성할 때 변수에 넣지 않고 CThread work_thread{CThread{ th_id, x, y }};
처럼 시스템 자원 객체를 바로 전달받아도 복사 생성자에서 필연적으로 const CThread&
임시 객체가 생성되어 버린다. 이때 보이지 않는 const CThread&
객체는 work_thread
에 시스템 자원을 순순히 넘겨주는 것처럼 보여도, 만약 소멸자에서 시스템 자원을 해제하도록 했다면 work_thread
는 생성하자마자 죽은 객체가 된다. 이를 막으려면 두가지 방법이 있다. 임시 객체인지 표시하는 플래그를 넣던가, 자원을 해제하는 전역 함수를 별도로 만들어야 하는데, 모두 최적화, 깔끔함 둘 다 만족시키지 못한다. &&
는 &를 하나 더 붙여 컴파일러와 사용자에게 복사, 참조와는 구분하게 하고 중복 자원의 문제도 깔끔하게 해결한다. 객체를 생성하는 방법을 하나 더 제시함으로써 많은 문제가 해결된 것이다.또한 한번 사용되면 바로 사라질 값이기에 중복해서 사용할 수 없다. 예를 들어 가변 배열인 표준 라이브러리의
std::vector
는 이동된 객체는 크기가 0으로 텅 비어버린다. 또다른 예로는 역시 표준 라이브러리의 std::thread
는 운영체제 자원을 사용하기에 복사할 수 없고, 오직 이동만 하도록 구현된다. 이를 이동시키면 내부 리소스가 새로운 객체에 전달되기에 원래 객체는 사용할 수 없다.9. 클래스 (Class)[편집]
import <string>;
import <print>;
class GreetingWorld
{
public:
GreetingWorld() noexcept
{
std::println(myGreet);
}
protected:
std::string myGreet = "Hello, world!";
};
"display: none; display: 문단=inline"를 참고하십시오.
10. 이름공간 (Namespace)[편집]
"display: none; display: 문단=inline"를 참고하십시오.
11. 메모리 할당[편집]
11.1. 자동 할당[편집]
<C++ 예제 보기> #include <utility> #include <iostream> int main() { int x = 0; // 0을 출력 std::cout << x << '\n'; // x의 주소를 출력 std::cout << std::addressof(x) << '\n'; return 0; }
lvalue
(참조)가 아닌 필드를 의미한다. 참고할 점은 함수의 반환값은 굳이 lvalue
를 명시하지 않으면 NVRO가 적용돼서 prvalue
또는 xvalue
가 되어 메모리 할당이 실제로 일어나지 않는다. 그 함수의 반환값이 실제로 사용될 때만 메모리 할당이 일어나는 것이다.예제에서는 "
int x = 0
"가 정수를 메모리에 할당하는것이다. 컴파일러의 구현마다 다르지만 보통 자동 할당은 운영체제 메모리의 스택에 저장된다. 이렇게 만들어진 객체는 함수의 범위(Scope)가 끝나면 회수된다. 참고로 std::array<std::array<T, S>>
같은 다차원 array
는 C++17 이후에서만 스스로 해제가 가능하므로 사용한 자료형에 따라 별도로 해제 해줘야 누수가 생기지 않는다.11.2. 정적 할당[편집]
<C++ 예제 보기> #include <cstdint> #include <utility> #include <string_view> // 전역 변수 float my_float; /*static*/ int my_st_integer; // static이어도 문제 없다. /*static*/ std::int64_t my_i64; /*static*/ std::int64_t* my_ptr_i64 = &my_i64; constexpr std::int32_t my_ct_i32 = 40; constexpr const std::int32_t* my_ct_ptr_i32 = &my_ct_i32; class MyClass public: static constexpr std::string_view Name = "MyClass"; static std::string_view NotCtName1; static inline std::string_view NotCtName2 = "MyClass"; constinit static inline std::string_view NotCtName3 = "MyClass"; }; int main() { /*constexpr*/ MyClass instance1, instance2; // constexpr 객체가 아니라도 constexpr 정적 필드는 컴파일 시간에 값을 검증할 수 있다. static_assert(std::addressof(instance1.NotCtName1) == std::addressof(instance2.NotCtName1)); // 정적 필드는 constexpr가 아니더라도 컴파일 시간에 주소를 검증할 수 있다. static_assert(std::addressof(instance1.NotCtName2) == std::addressof(instance2.NotCtName2)); static_assert(std::addressof(instance1.NotCtName3) == std::addressof(instance2.NotCtName3)); static_assert(sizeof(MyClass) == 1); // 그런데 소멸자는 호출 가능하다. // 다만 이렇게 명시적으로 호출하면 소멸자에 정의된 동작이 실행되며 실제로 메모리가 해제되는 시점은 함수의 범위가 끝나는 시점이다. instance1.~MyClass(); }
11.3. 동적 할당[편집]
#include <utility>
int* DanglingMemory()
{
int x = 0;
int *ptr = std::addressof(x); // 참조 대상 소실 (Dangling Pointer/Reference)
return ptr; // 접근 위반 오류 또는 경고. 보안 문제가 발생할 수 있다.
이 코드의 의도는 주소 '0'을 가리키는 포인터를 반환시키는 함수를 만드는것이다. 일단 자동 할당으로 정수 x
를 생성하고, 정수 포인터 ptr
에게 x
의 메모리 주소를 가리키라고 명령하고 ptr
를 반환하는 것. 하지만 위에서 서술했듯이, x
는 자동 할당으로 만들어진 자료기 때문에 함수의 범위를 벗어나면 x
에게 주어진 메모리는 운영체제가 회수한다. 요약하자면 실제로 이 함수를 사용하면 나오는건 0을 가리키는 포인터가 아니라 아무 말도 안 되는 걸 가리키는, 사용하면 안되는 포인터다. 이를 해결하는 방법 중 하나는 동적 메모리 할당 (Dynamic Memory Allocation)이다.이 역시 컴파일러의 구현마다 다르지만 보통 동적 할당은 스택대신 힙(Heap) 영역에 이루어진다. C++에서는
new 자료형;
형식의 표현식을 사용하면 된다. 조금 소개하자면 T *ptr = new T;
와 같이 쓸 수 있다. 위의 예제에서 의도하려 했던 함수를 만들려면 다음과 같이 코드를 짜면 된다.<C++ 예제 보기> #include <iostream> int* CreateMemory() { int *ptr = new int; // 동적 할당 *ptr = 0; return ptr; } int main() { int *ptr = CreateMemory(); // "0" 출력 std::cout << *ptr << '\n'; delete ptr; // ptr이 가리키는 메모리를 해제한다. 이제 ptr은 사용할 수 없다. }
delete
연산자를 사용하면 new
로 동적 할당한 메모리를 해제할 수 있다.유의할 점은 C언어/문법#동적 할당에서 소개하는
malloc
등으로 C++의 클래스를 건드리면 안된다. 왜냐하면 C++의 객체와 C의 메모리 공간은 엄연히 다른 존재이기 때문이다. new
는 C++의 런타임에서 따로 관리되는 메모리를 사용한다. C의 메모리 공간은 C++에서도 존재하며 똑같이 포인터로 표현된다. 반면 객체는 특정 메모리 공간을 특수한 용도로 사용하겠다고 지정한 상태를 말한다. 런타임 오버헤드 때문에 상대적으로 느리기도 하지만. 다시 말해서 new
는 바로 해당 메모리 공간에 어떤 클래스가 들어있고, 클래스 인스턴스의 수명(Lifetime)이 관리되며, 곧 생성자와 소멸자를 호출할거라고 선포한 상태란 것이다.결론적으로 C++의 객체를 C언어 방식으로 할당하면 생성자, 소멸자가 호출되지 않으므로, 정말 특수한 상황이 아니면
malloc
, free
등은 쓰지 말아야 한다. 객체 수명 관리가 안되므로 프로그램 동작이 꼬일 위험성이 높다.new는 메모리를 할당하는데 실패할 경우 std::bad_alloc 예외를 던진다. 예외 발생으로 인한 성능 저하를 걱정한다면 set_new_handler 함수를 통해 할당 실패 시 예외 대신 nullptr 값을 반환하도록 만들 수 있다.
반대로 이미 해제한 메모리를 재차 해제하려 하거나(Double-Free), 이미 해제한 메모리에 접근하려 할 경우(Use-After-Free)에도 치명적인 문제가 발생한다. 마지막으로
new
로 이미 할당 된 메모리(스택도 포함)에 또 할당 할 수도 있다. 해당 방법으로 스택 메모리에다가 할당한 경우, 따로 해제를 해 주지 않아도 된다. 다만 클래스라면 소멸자는 호출 해 주어야 한다. 그런데 만약 원래 동적 할당했던 필드에 할당했으면 이전에 할당했었던 메모리는 주소만 덮어 씌워지는 것이라 자동으로 해제되지 않는다! 때문에 반드시 이전에 할당했던 메모리를 해제해줘야만 한다. 그러나 이 작업이 여간 힘든 것이 아니므로 중복 할당은 피해야 한다.마지막으로 백엔드에서 서로 다른 런타임을 사용할 때는 경우 해제(
delete
)는 할당(new
)한 프로그램이 사용하는 런타임 영역 내에서 이루어 져야 한다. GCC가 사용하는 libstdc++, LLVM이 사용하는 libc++, 그리고 MSVC가 사용하는 msvcp는 서로 다른 ABI를 가지고 있으며 따라서 할당 방법도 다르다. libstdc++에서 할당한 포인터를 msvcp가 해제하는 경우 힙 커럽션등의 문제가 생길 수 있다.<C++ 예제 보기> #include <new> #include <memory> #include <span> struct MyData { void ExecuteData() const {} int a; int b; int c; }; struct MyData_Copy { void ExecuteData() const {} int a; int b; int c; }; struct Packet { [[nodiscard]] std::span<const char> Serialize() const noexcept { return std::span{ reinterpret_cast<const char*>(myData), mySize }; } void* myData; size_t mySize; }; int main() { MyData data{}; Packet packet1 { .myData = new (std::addressof(data)) MyData{}, .mySize = sizeof(MyData) }; // acq_data1_packet1는 data를 가리키는 포인터이다. MyData* acq_data1_packet1 = std::launder(reinterpret_cast<MyData*>(packet1.myData)); acq_data1_packet1->ExecuteData(); Packet packet2 { .myData = new MyData_Copy{}, // MyData과 똑같은 구성 .mySize = sizeof(MyData_Copy) }; // packet2.myData에는 MyData가 아닌 MyData_Copy가 들어있으므로 std::launder는 정의되지 않은 동작에 돌입한다. // 따라서 acq_data1_packet2의 값은 알 수 없다. MyData* acq_data1_packet2 = std::launder(reinterpret_cast<MyData*>(packet2.myData)); Packet packet3 { .myData = new MyData{}, .mySize = sizeof(MyData) }; delete packet3.myData; // packet3.myData는 해제된 메모리 이므로 std::launder는 정의되지 않은 동작에 돌입한다. // 따라서 acq_data1_packet3의 값은 알 수 없다. MyData* acq_data1_packet3 = std::launder(reinterpret_cast<MyData*>(packet3.myData)); }
std::launder
C++17 , std::start_lifetime_as
C++23 , std::start_lifetime_as_array
C++23 라는 메모리 공간을 객체로 취급하는 함수를 제공한다. 여기서 std::launder
는 실제로는 메모리 공간에 객체가 이미 있어야 하므로 조금 더 안정적인 reinterpret_cast
를 함수의 모양으로 제공하는 꼴이다.12. 언어 연결성 (Language Linkage)[편집]
"display: none; display: 문단=inline"를 참고하십시오.
13. 저장소 지속요건 (Storage Duration)[편집]
Storage Duration
13.1. static[편집]
정적인 기억공간 (
static
memory)static
이라는 단어는 정적이라는 뜻이다. 정적이라는 말은 객체가 있는 (가상) 메모리 위치가 (프로그램 내에서는) 변하지 않는다는 뜻이다. 곧 static
은 해당 객체가 프로그램 내부에서 영원불멸한 존재임을 보장한다. 변수와 함수에 사용할 수 있다.13.1.1. static 변수[편집]
정적인 내부 연결 변수 (Static Variable)
<C++ 예제 보기> namespace NamuWiki { class Squirrel { public: Squirrel(float x, float y, float z); float mx, my, mz; }; // (1) // NamuWiki::liveEverywhere로 언제든지 사용할 수 있다 static int liveEverywhere; // (2) // 오류! 기본 생성자를 호출할 수 없습니다 static Squirrel allIsNotWell; } // 이름 공간과 클래스 안에 있는 정적 변수의 정의는 스코프 밖에, 그리고 헤더라면 별도의 소스 파일에 작성해야 한다. // 그러나 liveEverywhere이 정말 이때 초기화될지는 확실하지 않다. int NamuWiki::liveEverywhere = 40;
static
, 그리고 선택적으로 inline
지시자를 덧붙일 수 있다. static
변수는 값을 할당해주거나 기본값을 할당할 수 있어야 한다. static
변수를 함수에서 선언했다면 맨 처음에 생성될 때만 값이 할당된다. 만약 static
변수가 클래스의 인스턴스이고, 초기값을 할당하지 않았다면 그 변수는 클래스의 기본 생성자를 호출한다 [8] . 때문에 기본 생성자가 없으면 오류가 발생할 수 있으니 주의해야 한다.주의해야할 점은
static
변수는 초기화되는 시점이 불분명하다는 것이다. C언어부터 내려오는 유서깊은 문제였으나 지금도 해결되지 않았다. 때문에 분명히 소스 파일에 값을 전달했건만 그 값을 못 읽는 경우가 있다는 것이다. C++11에서 이를 대체하기 위한 inline constexpr
와 장황한 static inline constexpr
변수가 등장했으나 이는 컴파일 상수가 될 수 있는 객체만 허용하며 객체 초기화에 ()
를 쓰면 함수 정의와 헷갈리는 문제가 있었다. C++20에는 변수 초기화 전용으로 constinit
이 추가됐다.13.1.2. static 함수[편집]
정적인 내부 연결 함수 (Static Function)
<C++ 예제 보기> import <string_view>; // 구현 내용이 없으면 링크 오류가 발생한다. static char GetFirstCharacter(std::string_view str); // 헤더를 여러번 삽입하더라도 GetFirstCharacter는 언제나 동일한 함수임이 보장된다. // 내부 연결 객체는 현재 이름공간에 유일한 존재로 남으므로 식별자의 중복 선언 문제가 발생하지 않는다. static char GetFirstCharacter(std::string_view str) { return str.front(); }
static
, 그리고 선택적으로 inline
지시자를 덧붙여 해당 함수가 정적인 내부 연결을 가진 함수임을 나타낸다. 원래 C의 소스 구조에서는 여러 곳에서 중복 삽입될 수 있는 헤더는 중복된 객체 링킹 때문에 오직 전방위 선언만을 사용해야했다. 이를 해결하기 위해 static
또는 extern
을 소스 파일이 아니라 헤더 파일에서 함수와 변수를 정의하기 위해 사용해왔다. 다만 이젠 이름공간을 사용할 수 있기 때문에 의미가 퇴색된 바 있다. 그리고 C++20에서 소개된 모듈에선 static
함수는 모듈 밖으로 내보낼 수 없다! 모듈 밖으로 객체를 내보내는 export
구문은 외부 연결인데 static
함수는 내부 연결이라서 링크 오류가 발생한다.static
함수는 정적인 메모리의 특징과 함께 내부 연결이라서 전방위 선언만 할 수 없고 inline
처럼 구현도 해줘야 한다. 그런데 inline
은 사용자가 함수의 정의를 알아서 고쳐야 했지만 static
은 그 자체로 함수의 특징을 강제한다. 그래서 현재 해석 단위[헤더] 에서는 이름이 같은 함수이면 무조건 동일한 함수를 의미하고, 프로그램 내내 일정한 메모리 주소에 위치함을 알 수 있다. 그렇기에 static
함수가 헤더에 정의되어 있을 때에는 여러 곳에서 헤더를 삽입해도 링크 오류없이 사용할 수 있다13.1.3. 클래스 멤버[편집]
<C++ 예제 보기> class GameObject { public: static inline const int firstId = 1000; static const std::string DefaultName; static GameObject* CreateObject() { // objectId는 단 한번 할당된다 static int objectId = firstId; return new GameObject{ .myID = objectId++, .myName = DefaultName }; } int myID; std::string myName; }; template<typename crtp> class ISingleton { public: static void SetInstance(crtp* inst) { if (Instance == nullptr) { crtp = inst; } else { throw "Singleton error!"; } } [[nodiscard]] static crtp* GetInstance() noexcept { return Instance; } protected: static crtp* Instance = nullptr; } std::string GameObject::DefaultName = "GameObject";
클래스의 정적인 멤버는 외부 연결로 취급된다. 이 규칙이 없으면 헤더와 모듈C++20 에서 정적 멤버를 사용할 수 없을 것이다. 정적 멤버의 종류는
using
과 typedef
의 자료형 별칭, 정적 데이터 멤버, 정적 멤버 함수가 있다.클래스 내의 정적 멤버는 클래스 인스턴스를 만들 필요도 없이 클래스 이름만으로 사용할 수 있다. C++에는 메타클래스가 없으므로 클래스 자체는 메모리에 정적으로 고정된 객체다. 그리고 클래스의 고정된 메모리 주소로부터 상대적 위치만 저장해서 멤버를 구별한다. 그래서 C++의 클래스는 정적 멤버가 아니더라도 일단
MyPhone::callNumber
와 같이 이름만이라도 접근할 수는 있다. 그러나 컴파일 오류가 발생하거나 메모리 접근 위반 0x00000016 참조!
따위의 런타임 오류가 발생할 것이다. 여기서 0x00000016
이 클래스 MyPhone
에 대한 필드 callNumber
의 상대적 주소다. 여기서 정적 멤버는 고정된 클래스 메모리 주소 + 고정된 멤버 메모리 주소가 합쳐져 프로그램 내내 일정한 메모리 위치에 존재한다. 때문에 프로그램 안에서는 문맥 상관없이 모두가 참조할 수 있는 객체가 된다. 그런데 이게 외부 연결의 특징을 띄기에 클래스의 정적 멤버가 외부 연결로 취급되는 것이다.static
멤버가 가장 잘 활용되는 곳은 싱글톤 패턴 클래스다. 싱글톤은 프로그램에서 클래스에 대해 유일하게 존재하는 인스턴스를 구현하는 패턴인데, static
이 정확하게 이 목적에 부합한다.13.2. extern[편집]
<C++ 예제 보기> extern class GameObject;
정적인 외부 연결 (
extern
al Linkage)객체 선언의 자료형 앞에
extern
, 그리고 선택적으로 inline
지시자를 덧붙일 수 있다.extern
은 객체가 정적인 외부 연결이라고 나타낼 수 있다. 그런데 static
과는 반대되는 기능이 아님을 명심해야 한다. extern
변수, extern
함수도 정적인 객체이며 단지 정의를 외부에서 가져올 수 있다는 뜻이다. extern
도 정적인 이유는 외부 인터페이스에서 객체(변수, 함수)에 접근했을 때, 해당 객체가 이전에 같은 이름으로 접근했던 바로 그 존재임이 확실하지 않기 때문이다. 정적인 객체는 한번 할당되면 다시는 새로운 주소가 할당되지 않으므로, 외부 연결임에도 동일한 객체를 유지하는 목적이 있다. 때문에 C++17에서 static
대신 내부 연결만을 적용하기 위해 이름없는 이름공간이 도입되었는데, extern
은 외부 연결만 적용하는 키워드가 없다. 내부 코드가 바뀔 수 있는 위험성이 있으므로.두번째 기능도 있는데, 템플릿 실체화(Template Instantiation) 기능이다. 템플릿의 구현을 해당 소스에서만 컴파일하도록 지시한다. 템플릿은 모든 자료형 후보에 대해 컴파일을 시도하는데, 이러면 컴파일 시간이 기하급수적으로 늘어날 수 있다. 이때 원하는 특수화 후보를 명시하면 중복되는 컴파일 시도를 줄일 수 있다.
세번째로 C와 C++를 언어 전환하는 기능도 있다.
extern "C"
, extern "C++"
[9] 과 같이 사용한다. 기본적으로 C++의 모든 객체에는 extern "C++"
이 적용된다. 모든 이름공간, 클래스와 변수 앞에 보이지 않는 extern "C++"
이 붙어있다고 생각하면 된다. 그리고 extern "C"
를 사용하면 함수 오버로딩 금지 등 C언어의 규칙을 따로 적용할 수 있다 [10] .해당 키워드를 가장 많이 쓰는 경우는 ABI가 호환되는 라이브러리를 만들 때로, 예를 들어
void* myFunction(void* ptr, const void* cptr, int x)
의 경우, MSVC는 ?myFunction@@YAPEAXPEAXPEBXH@Z
, clang/GCC는 _Z10myFunctionPvPKvi
로 심볼이 변환되게 되는데 (Mangling) extern "C"
선언을 통해 본 함수명이자 C 방식인 myFunction
이라는 심볼명을 그대로 사용하여 dlsym()이나 다른 프로그래밍 언어에서 C++로 작성된 코드를 불러와 사용하는 것이 가능해진다.13.3. thread_local[편집]
<C++ 예제 보기> import <vector>; import <thread>; import <chrono>; import <print>; // 전역 범위에 선언되어 있지만, 실제로는 스레드 단위 지역 변수다. thread_local size_t threadID; thread_local size_t threadCount = 0; void Watcher(size_t id) { // 보이지 않는 threadID, threadCount 지역 변수가 선언되어 있다. threadID = id; using namespace std::chrono_literals; while (true) { if (::rand() % 10 == 0) { std::println("스레드 ID {}에서 {}번째 보고", threadID, ++threadCount); } std::this_thread::sleep_for(1s); } } int main() { std::vector<std::jthread> myThreads{}; myThreads.reserve(4); for (size_t i = 0; i < 4; ++i) { myThreads.emplace_back(Watcher, i); } while (true) { std::this_thread::yield(); } }
이름공간[11] , 클래스에서 변수 선언에 사용할 수 있다. 전역 변수 선언에 사용하면 이 순간부터 프로그램에서 사용한 모든 스레드에 해당 변수가 선언되게 된다. 참고로 내외부 연결성에 관여하지는 않으므로,
static
이나 extern
과 조합해서 쓸 수 있다.13.4. mutable[편집]
<C++ 예제 보기> import <atomic>; import <vector>; import <thread>; import <print>; class SpinLock { public: constexpr SpinLock() noexcept = default; ~SpinLock() noexcept = default; // const 메서드이지만 myState를 수정하고 있다. void Lock(std::memory_order model = std::memory_order::memory_order_acquire) const volatile noexcept { while (!TryLock(model)); } bool TryLock(std::memory_order model = std::memory_order::memory_order_relaxed) const volatile noexcept { return !mySwitch.test_and_set(model); } void Unlock(std::memory_order model = std::memory_order::memory_order_release) const volatile noexcept { mySwitch.clear(model); } [[nodiscard]] bool IsLocked() const noexcept { return mySwitch.test(std::memory_order::memory_order_relaxed); } SpinLock(const SpinLock&) = delete; SpinLock& operator=(const SpinLock&) = delete; private: mutable volatile std::atomic_flag mySwitch; }; // const여도 아무 문제 없다. const SpinLock globalLock{}; size_t globalCounter = 0; void Incrementor(size_t max) noexcept { for (size_t i = 0; i < max; ++i) { globalLock.Lock(); globalCounter++; globalLock.Unlock(); } } int main() { constexpr size_t target = 1000'0000'0000'0000; constexpr size_t thrd_count = 10; constexpr size_t thrd_workout = target / thrd_count; std::vector<std::thread> myThreads{}; myThreads.reserve(thrd_count); for (size_t i = 0; i < thrd_count; ++i) { myThreads.emplace(Incrementor, thrd_workout); } for (auto& th : myThreads) { th.join(); } std::println("결과: {}", globalCounter); // 1000000000000000 return 0; }
클래스의 필드에 사용할 수 있다.
const
한정자와는 같이 적용할 수 없다. 이 요건이 적용된 필드는 const
인스턴스, const
메서드에서도 수정할 수 있다. 상단의 예제는 대표적으로 쓰이는 스핀락의 구현이다.14. 상수 표현식 (Constant expression)[편집]
C++11 부터 도입된 상수 표현식, 또는 상수식은 실제 코드가 실행되는 사용자 시점(런타임)이 아니라 컴파일 시점으로 코드의 평가를 앞당길 수 있는 획기적인 기능이다. C++의 킬러 요소라고 말할 수 있는 핵심 기능이다 [12] . 바이너리가 생성되는 컴파일 시점에 실행 결과가 결정되기 때문에 아무런 평가 과정도 컴파일 이후에 남지 않는다. 곧 프로그램 이용자의 실행 시점에는 코드 실행 시간이 0이 되도록 최적화된다.
"display: none; display: 문단=inline"를 참고하십시오.
15. 제약조건[편집]
Requires Clause (requires 절)
16. 값 범주론 (Value Category)[편집]
"display: none; display: 문단=inline"를 참고하십시오.
17. 템플릿 (Template)[편집]
자료형, 함수, 클래스를 모두 읽고 오는 것을 추천한다.
17.1. auto[편집]
C++에서 일반 사용자가 가장 유용하게 사용할 수 있는 기능 중에 하나다. C++에서 극히 희귀한 문법적 설탕 요소이며 자료형을 일일이 명시해야하는 문제를 해결하기 위해 도입되었다. 라이브러리를 직접 작성하는 것이 아니라면 클라이언트 단에서는 적극적으로 사용해도 큰 문제는 없다. 사용법은 변수와 함수에서 자료형을 작성할 때 대신
auto
를 기입하면 된다. 컴파일러가 해당 변수의 자료형, 함수의 반환형을 추론하여 바이너리에 알아서 반영해준다.주의할 점은 구조체의 멤버 변수, 함수의 선언 시에는 사용이 불가능하다는 것이다.
auto
가 어떤 자료형인지는 컴파일 시점에 결정되는데 auto
자체는 마치 참조 변수마냥 원래 쓰일 자료형의 별칭이기 때문이다. 참조 변수처럼 auto
혼자서는 존재할 수 없다. 따라서 클래스의 필드 자료형은 모두 명시해야 한다.지금까지 본 함수 예제에서 전달한 인자들이 복사된다는 사실을 알았다.
auto
도 같은 맥락의 규칙을 따른다. auto
를 쓰면 &
, &&
, C++ 배열[]
이 모두 증발한 자료형이 추론된다. 가령 void Function(auto value)
에서 매개변수 value
는 값에 의한 전달을 수행한다. 또 반환형에 한정자가 붙은 함수를 auto
로 값을 받으면 *
를 제외한 한정자가 무시된다. 사용자가 직접 &
, const&
, &&
*
따위의 한정자를 기입하지 않으면 컴파일러는 무조건 값에 의한 전달을 수행한다. 이를 정확한 용어로는 부패 (Decay)라고 한다. 부패는 자료형에 붙은 모든 한정자가 날아가서 정확한 자료형이 소실된 상태를 말한다. 이렇게 해야 함수 내부의 값과 외부의 값을 분리하고 의도치 않은 동작을 막을 수 있다. C++에서 의도하지 않은 동작은 모두 일어나서는 안되는 일이다. 부패를 막으려면 템플릿에 대한 이해가 필요하다. 자세한 내용은 다음 단락에서 설명한다.<C++ 예제 보기> class Counter { public: constexpr Counter() noexcept = default; constexpr ~Counter() noexcept = default; constexpr Counter(const size_t& number) noexcept : myNumber(number) {} constexpr size_t& GetNumber() noexcept { return myNumber; } constexpr const size_t& GetNumber() const noexcept { return myNumber; } constexpr auto GetNumber2() noexcept { return myNumber; } // 한정자 없는 size_t 반환 constexpr auto GetNumber2() const noexcept { return myNumber; } // 한정자 없는 size_t 반환 protected: size_t myNumber = 0; } int main() { Counter counter_v{ 300 }; constexpr Counter counter_c{ 500 }; auto cnt1 = counter_v.GetNumber(); // size_t const auto cnt2 = counter_v.GetNumber(); // const size_t const auto& cnt3 = counter_v.GetNumber(); // counter_v.myNumber 필드의 불변 참조 변수다. size_t cnt4 = counter_v.GetNumber(); size_t& cnt5 = counter_v.GetNumber(); // cnt5를 통해 counter_v.myNumber 필드를 수정할 수 있다. const size_t& cnt6 = counter_v.GetNumber(); // counter_v.myNumber 필드의 불변 참조 변수다. auto cnt7 = counter_c.GetNumber(); // size_t auto& cnt8 = Counter{ 700 }.GetNumber(); // 형식 한정자 오류! const auto& cnt8 = counter_c.GetNumber(); // const size_t& 이며 counter_c.myNumber 필드의 참조 변수다. }
*
, &
, &&
, const
, volatile
를 직접 붙일 수 있다. 그러나 auto
를 쓰는 이유가 뭔지 생각해본다면 조금 아쉬운 면이 있다. 실제 코드에서는 const&
또는 때때로 헷갈림 방지를 위해 *
말고는 더 붙일 일이 없을 것이다.17.2. 템플릿 인자 추론[편집]
<C++ 예제 보기> import <string>; import <print>; template<typename T> void increment1(T x) { // 전위 증가 연산자를 사용할 수 없으면 예외 발생 ++x; } void increment2(auto x) { // 전위 증가 연산자를 사용할 수 없으면 예외 발생 ++x; } template<typename T> void increment3(T lhs, T rhs) { lhs += rhs; } template<typename T> void increment4(T lhs, const T* rhs) { lhs += *rhs; } void increment5(auto lhs, auto rhs) { lhs += rhs; } int main() { int a = 100; long long b = 500; const int c = 900; int& d = a; increment1(a); // 아무것도 안 함 (1) increment1(510942633); // (2) increment1(b); // (3) increment1(d); // (4) d는 a의 참조 변수이지만 &가 부패해서 사라진다 increment1('B'); // (5) increment1("namu"); // 오류! 문자열은 더할 수 없습니다 increment2(a); // (4) increment2(a + b); // (5) 값에 의한 전달은 prvalue도 전달할 수 있다 increment2('B'); // (6) increment2("wiki"); // 오류! 문자열은 더할 수 없습니다 increment3(a, 1058142); // (7) increment3(a, c); // (8) 인자의 const는 매개변수의 auto에 영향을 끼치지 못한다 increment3(a, b); // 오류! 전달된 두 매개변수 T의 자료형이 서로 다릅니다 increment4(a, &c); // (9) 포인터(주소)는 glvalue만이 가질 수 있다. glvalue는 lvalue라서 모든 한정자를 반드시 유지한다 increment4(a, &b); // 오류! 전달된 두 매개변수 T의 자료형이 서로 다릅니다 increment4(a, &d); // 오류! 포인터와 인자의 const 한정자가 일치하지 않습니다 increment5(a, b); // (10) increment5(d, c); // (11) d는 참조형이지만 auto에서 &가 부패해서 사라진다 increment5(c, d); // (12) increment5(d, b); // (13) increment5(std::string{ "Namu" }, std::string{ "Wiki" }); // (14) std::println("a의 값: {}", a) // 100 std::println("b의 값: {}", b) // 500 std::println("c의 값: {}", c) // 900 std::println("d의 값: {}", d) // 100 (a의 참조형) return 0; }
increment1
함수와 increment2
함수는 서로 같은 의미를 가진다. 이게 중요한 이유는 바로 템플릿과 auto
는 바로 똑같은 존재임을 뜻하기 때문이다. auto
는 바로 각각 다른 자료형으로 추론되는 템플릿을 간편하게 쓸 수 있게 해주는 문법적 설탕이다. auto
를 사용한 변수는 템플릿 변수이며 auto
가 사용된 함수는 템플릿 함수다. 그렇기에 auto
나 템플릿이나 원래의 한정자가 부패하는 것이다. 이를 막으려면 완벽한 자료형을 얻어야 한다. 그러나 auto
에 한정자를 붙이는 작업은 불완전하고 실수를 유발하기 쉽다. 자료형 기입이 아무리 쉬워졌다고 해도 여전히 반복적이고 번거로운 작업이기 때문이다. 이러면 처음으로 돌아가 원래의 온전한 자료형을 쓰는 게 더 이롭다. 다음 단락에서는 여러 방법을 소개한다.17.2.1. decltype[편집]
17.2.1.1. decltype(auto)[편집]
<C++ 예제 보기> int main() { Counter counter_v{ 300 }; constexpr Counter counter_c{ 500 }; decltype(auto) cnt1 = counter_v.GetNumber(); // size_t& counter_v.myNumber 필드의 참조 변수다. decltype(auto) cnt2 = counter_c.GetNumber(); // const size_t& counter_c.myNumber 필드의 참조 변수다. const decltype(auto)& cnt3 = counter_v.GetNumber(); // 오류! decltype(auto)는 한정자를 붙일 수 없다. }
auto
는 조금 부족한 면이 있었다. auto
혼자서는 온전한 자료형을 얻을 수 없기 때문이다. 불필요한 복사가 발생하는 문제도 있다. 해결책으로는 const&
를 붙이는 방법이 있지만 번거롭고 반복적인 작업일 뿐이다. 이에 도입된 decltype(auto)
는 참조형까지 전부 완전한 자료형을 가져온다. auto
대신에 사용하면 된다. 참고로 decltype(auto)
는 그 자체로 완성된 자료형을 가져오므로 형식 한정자를 더 붙일 수 없다. 원한다면 using
을 써서 자료형을 붙여야 한다.17.2.1.2. decltype(expression)[편집]
인자로 전달한 표현식을 묵시적으로 평가한 결과의 자료형을 얻는다. 가령
decltype(50 + 400U)
는 unsigned int
를 반환한다. 여기서 묵시적으로 평가했다는 말은 표현식이 실제로 실행되지는 않는다는 뜻이다. 오직 반환형만을 알 수 있다. 예를 들어서 decltype((int*)malloc(50000000))
같은 무지막지한 식이라도 얌전히 int*
를 반환할 것이다. 그리고 여기서 알 수 있듯이 표현식이 반드시 상수 표현식일 필요는 없다. 성능 문제가 있을까 싶지만 사용자 단에서 반환형이 무엇인지 알 수 없더라도, C++ 타입 시스템은 반드시 컴파일 시점에 모든 자료형을 확정짓는다.이것이 유용한 예로는 정수와 큰 정수의 연산, 실수와 정수 사이의 연산, 비용이 큰 연산을 하기 전에 자료형을 가져와서 미리 준비를 할때다. 서로 다른 정수 사이에는 더 바이트 수가 큰 정수로 승급하는 규칙이 있고, 실수와 정수 사이에는 실수로 변환되는 규칙이 있다. 비용이 큰 연산에는 직렬화, 문자열 포맷 등이 있다. 직렬화를 하려면 원본 자료형을 더 작은 자료형으로 변환하거나, 일정한 크기의 메모리에 변환된 값들을 쓰는 과정이 필요하다. 이때 실제로 변환 작업을 하기 전에 먼저 필요한 메모리를 할당하는 수가 있다. 표준 라이브러리에서 사용하는 예로, 시간 라이브러리
<chrono>
에서 서로 다른 시간 단위를 서로 연산할 때, 불필요한 시간 변환 작업을 줄이기 위해 decltype(lhs + rhs)
와 using
을 사용하여 반환형을 가져온다. 이것 역시 auto
대신에 사용하면 된다.17.2.1.3. 함수 응용 예제[편집]
반환 자료형은 함수의 실행 결과로 어떤 종류의 값이 나오는지 나타내는 자료형이다. 함수를 정의할 때는 식별자 앞에 반환 자료형을 넣어야 한다. 이때 직접 자료형을 기입하거나
auto
C++11 혹은 decltype(expr))
C++11 키워드를 사용할 수 있다. 만약 반환하는 값이 없다면 void
를 넣으면 된다. 주의할 점은 자료형 문서에서 말했던 것 처럼 auto
와 decltype(auto)
은 스스로 존재할 수 없는 자료형의 별칭이라는 것이다. 그래서 auto
와 decltype(auto)
을 이용할 때는 함수의 구현부가 필요하다.auto
를 사용한 경우 함수의 닫는 소괄호 맨 뒤쪽에 ->
와 함께 반환 자료형을 적을 수 있다. 자료형의 한정자 때문에 auto
를 못써서 자료형을 명시할 필요가 있으나 단번에 자료형을 알기 어려우면, auto Add(T t, U u) -> decltype(t + u)
처럼 작성할 수 있다.만약
decltype(auto)
을 사용하면 반환형을 값 범주(Value Category)까지 완벽하게 보존해서 반환된다. 반환하는 자료형 원본을 T
라고 했을 때 반환하는 값이 리터럴같은 prvalue
면 T
로 추론된다. 메모리에만 있는 이름없는 객체나 우측 참조자 형변환 따위의 xvalue
라면 T&&
로 추론된다. 클래스의 필드나 함수 외부의 변수 혹은 전역 변수같이 이름이 있는 lvalue
라면 T&
로 추론된다.17.2.2. 완벽한 매개변수 전달[편집]
<C++ 예제 보기> import <type_traits>; void ValueFunction(auto value); void LvalueFunction(auto& value); void RvalueFunction(auto&& value); auto&& Function4_forwarding(auto&& value) { // 복사, &, &&, []가 모두 사라짐 (Decay) // 복사할 수 없는 값이라면 오류 발생함 return value; // std::move는 lvalue를 보존하지 않기 때문에 문제가 생긴다. // value가 glvalue // T&: T&& // const T&: lvalue는 const T&, xvalue는 const T&& // value가 rvalue // T&& - T&& // const T&& - const T&& return std::move(value); } int main() { const long A = 132435; ValueFunction(A); // value는 long ValueFunction(std::move(A)); // value는 long ValueFunction(8000); // 리터럴 value는 int LvalueFunction(A); // value&는 const long& LvalueFunction(std::move(A)); // value&는 const long& LvalueFunction(8000); // 오류! 리터럴은 lvalue에 대입할 수 없음 RvalueFunction(A); // value&&는 const long& RvalueFunction(std::move(A)); // value&&는 const long&& RvalueFunction(8000); // 리터럴 value&&는 int&& }
auto
는 인자의 자료형을 썩히고(Decay), *
혹은 순수한 자료형만 보존한다. 즉 const
, volatile
, &
, &&
는 무시하고 값으로 전달을 시행한다. 왜냐하면 썩힌다는 것은 최소한의 의미만 남기고 자료형을 날린다는 것인데, 단일 const
, volatile
은 함수에 전달된 이상 아무 의미가 없기 때문이다. 참조형이 아니라면 그게 상수던 휘발성이던 값으로 전달될 것이고, 그럼 복사가 되든 이동이 되든지 간에, 인자로 전달된 순간부터는 함수 안에서 밖으로 영향을 끼치지 못한다. 사용자 단에서도 const
는 단지 코딩에서 실수를 줄이거나 모호함을 줄이기 위해 구태여 붙이는 한정자이지, 인자로 전달됐던 원본 값이랑은 전혀 연관이 없는 변수가 된다. const
, volatile
, &
, &&
은 서로 보완하지 않으면 함수 안에서는 아무 의미를 갖지 못한다.그래서 사용자가
auto&
로 지정하면 &
에 의존하는 모든 한정자가 딸려나온다. 굳이 const volatile
을 붙이지 않아도 말이다. 그러나 const
또는 volatile
가 없는 auto&
는 무조건 lvalue
가 되어서 &&
로 표현되는 리터럴과 임시값을 넣을 수 없다. 예를 들어서 예제의 LvalueFunction
에는 500
, int(120648395)
같은 값을 전달할 수 없다. 그럼 좌측값, 우측값 매개변수 구분을 위해 const& T
, T&&
를 모두 오버로딩해야만 할까? 사실 그렇지 않다. 가령 예제의 RvalueFunction
함수는 rvalue
만 받을 수 있을 것 같지만, auto&&
는 모든 한정자에 대해 사용할 수 있다.<C++ 예제 보기> import <utility>; template<typename T> T&& Function5_forwarding_by_template(T&& value) noexcept(noexcept(std::declval<T&&>())) // 원본 자료형을 유지한채 value의 객체가 생성될 때 예외가 없음을 확인한다 { // lvalue, xvalue, prvalue 모두가 원래 값 범주(Value Category)를 유지한채, 아무 비용없이 전달된다 // lvalue는 lvalue 그대로 전달된다 // xvalue를 감싸 이름없이 전달한다 // prvalue를 감싸 이름없이 전달한다 return std::forward<T>(value); } template<typename T, typename V> auto&& Function5_modified_forwarding_by_template(V&& value) noexcept(noexcept(std::forward_like<const volatile T>(std::declval<V&&>()))) // 원본 자료형을 바꾼 value의 객체가 생성될 때 예외가 없음을 확인한다 { // 원래 값 범주를 유지한채로, 다른 자료형으로 바꿔 전달할 수 있다. return std::forward_like<const volatile T>(value); } // C++23부터 사용할 수 있는 Function5_forwarding_by_template과 같은 코드 auto&& Function5_forwarding_by_deduction(auto&& value) noexcept(noexcept(std::declval<decltype(value)>())) // 원본 자료형 그대로 value의 객체가 생성될 때 예외가 없음을 확인한다 { // C++23부터 가능한 완벽한 전달 수단 return auto{ value }; // 또는 return auto(value); } Position Function6_forwarding_by_copy(const Position& pos) noexcept(std::is_nothrow_copy_constructible<Position>) { // pos를 복사해서 전달한다 return pos; } Position&& Function6_forwarding_by_move(Position&& pos) noexcept(std::is_nothrow_move_constructible<Position>) { // pos를 아무 성능 오버헤드 없이 그대로 전달한다 return std::move(pos); // 이동 연산에 써도 문제 없다. 그러나 lvalue가 아님을 유의해야 한다 return std::forward<Position>(pos); // 경고! 이 경우 복사가 되어 참조 Dangling이 일어난다 return pos; }
&
, &&
를 멀쩡하게 갖고 있다. 그리고 이때 &가 여러번 중첩될 경우 &
또는 &&
중 한 가지 경우로 압축한다. &
는 &&
앞에 있으면 &&
가 되어버린다. 다시 말해서 static_cast<T&&>(T&)
는 T&&
로 연역된다. static_cast<const T&&>(T&)
는 const T&&
로 연역된다. 이 특성은 특이하게도 매개변수의 원본 자료형을 그대로 보존하는 효과가 나온다. 덕분에 auto
에서 원본 자료형이 뭔지 알기 위해 decltype(auto)
을 쓸 필요가 없다. 그리고 const auto&
, auto&&
를 모두 오버로딩 할 필요가 없다. 매개변수가 뭔지, 복사해야 할지 참조해야 할지 이동시켜야 할지 고민할 필요를 없애준다.17.2.3. 사용 예제[편집]
<C++ 예제 보기> // 매개 변수가 있고 반환값은 없는 함수 template<typename T> void SetID(const T& obj, unsigned long long id) { obj.id = id; } // 사용자 정의 noexcept 명세를 사용하는 함수 inline constexpr size_t MySize = 10; int MyBuffer[MySize]{}; template<size_t Index> constexpr int& Set(const int& value) noexcept(Index < MySize) // Index가 MySize보다 작으면 오류가 없다. { // 그러나 예외를 잡아내는 코드를 생성하지 않는다는 거지, 예외가 발생하지 않도록 하는 건 아니다. // 여전히 Index가 MySize 이상이면 오류가 발생한다. // 그냥 noexcept로 지정하면, 메모리 접근 위반이 발생했을때 예외 알림 대신 프로그램이 종료된다. return MyBuffer[Index] = value; } // 후속 반환형을 사용하는 함수 // 제약조건, noexcept 명세, 후속 반환형 사용 template<typename T, size_t Size> requires std::copyable<T> // <concept> constexpr auto CreateArray(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) // <type_traits> -> std::array<T, Size> { // <array> std::array<T, size> result{}; // <ranges> std::ranges::fill(result, value); return result; // Return Value Optimization 적용 } // 템플릿, constexpr, 조건부 noexcept 사용 template<typename T> constexpr T Instantiate(T&& obj, const float x, const float y, const float z) noexcept(std::is_nothrow_constructible_v<T, std::add_rvalue_reference_t<T>>) { // 완벽한 전달(Perfect Forwarding) 사용 return T(std::forward<T>(obj), x, y, z); } // 가변 템플릿, constexpr, 조건부 noexcept, 후속 반환형 사용 template<typename... Ts> constexpr auto ForwardAsTuple(Ts&&... args) noexcept(std::conjunction_v<std::is_nothrow_constructible<Ts, Ts&&>...>) -> std::tuple<Ts&&...> { // 완벽한 전달(Perfect Forwarding) 사용 return std::tuple<Ts&&...>{ std::forward<Ts>(args)... }; } int main() { const long A = 0; // 0 unsigned long long B = 93140583732; // 1 bool C = false; // 2 bool& D = C; // 3 Squirrel E{}; // 4 const Squirrel& F = E; // 5 constexpr unsigned G = 34275860428; // 6 auto tuple1 = std::make_tuple(A, B, C, D, E, F, G); std::get<2>(tuple1) = true; // 복사본은 원본 C, D에 영향을 주지 못함 auto tuple2 = ForwardAsTuple(A, std::move(B), C, D, E, F, Squirrel{}, std::move(G)); std::get<2>(tuple2) = true; // C, D가 true가 됨 std::get<4>(tuple2).myName = "new name"; // E, F의 myName이 "new name"이 됨 return 0; }
tuple1
은 복사본 튜플이 되어 std::tuple<long, unsigned long long, bool, bool, Squirrel, Squirrel, unsigned>
로 생성된다. 그러나 tuple2
는 완벽한 전달을 수행하여 std::tuple<const long&, unsigned long long&&, bool&, bool&, Squirrel&, const Squirrel&, Squirrel&&, const unsigned&&>
가 된다. 참고로 이 기능을 수행하는 함수는 표준 라이브러리에 이미 std::forward_as_tuple
, std::tie
가 있으므로 굳이 또 구현할 필요는 없다.17.3. 문자열 리터럴[편집]
void Function1(const char* string);
void Function2(const char* const& string);
template<size_t Length>
void Function3(const char (&string)[Length]);
void Function4(auto string);
int main()
{
auto str1 = "wasd"; // 문자열 리터럴은 반드시 const char*
const auto str2 = "ijkl"; // const char* const
constexpr auto str3 = "zxcv"; // const char* const&
Function1("Hello, world! (1)"); // string은 null로 끝나는 문자열
Function2("Hello, world! (2)"); // string은 null로 끝나는 문자열
Function3("Hello, world! (3)"); // string은 char의 크기가 18인 문자열
Function4("Hello, world! (4)"); // string은 null로 끝나는 문자열, const char*
return 0;
}
C++의 문자열 리터럴은 lvalue
인 const (&char)[Size]
로 연역된다. 컴파일러에 따라 다르지만, 문자열 리터럴은 실제로 변수에 전달되기 전에는 메모리에 존재하지 않는 값이다. 또는 최적화를 위해 프로그램의 문자열 리터럴을 한데 모은 문자열 풀(Pool)을 구성하고 가져다 쓰기도 한다. C언어 까지는 char[Size]
또는 여기서 연역된 char*
였으나, 이는 실제로 수정할 수 없는 리터럴에 대한 오해를 사게 만들었다. 현재 C++의 문자열 리터럴은 C에서 계승되는 포인터-배열 연역 법칙인 const char*
, const&
가 const char*
를 받는 const char* const&
, 그리고 const lvalue
인 const char[Size]
혹은 const (&char)[Size]
로 나타내진다. 참고로 const char[Size]
는 문자열 배열 안의 값은 복사가 일어나진 않지만, 문자열 배열 식별자의 포인터는 복사가 된다. 사소한 사항이고, 성능에 영향도 거의 없지만, 어쨌든 &
로 받는 걸 추천한다.17.4. 클래스[편집]
17.4.1. 템플릿 데이터 멤버[편집]
17.4.2. 템플릿 멤버 함수[편집]
17.4.2.1. Deducing this[편집]
17.4.3. 템플릿 자료형 멤버[편집]
template<typename T>
class Trait
{
public:
using type = T;
using value_type = T;
using const_type = const T;
using reference = T&;
using const_reference = const T&;
using rvalue_reference = T&&;
using const_rvalue_reference = const T&&;
using pointer = T*;
using const_pointer = const T*;
using difference_type = std::ptrdiff_t;
};
template<typename _Ty, typename _Trait = Trait<_Ty>>
class MyVector
{
public:
using value_type = _Trait<_Ty>::value_type;
using const_type = _Trait<_Ty>::const_type;
using reference = _Trait<_Ty>::reference;
using const_reference = _Trait<_Ty>::const_reference;
using rvalue_reference = _Trait<_Ty>::rvalue_reference;
using const_rvalue_reference = _Trait<_Ty>::const_rvalue_reference;
using pointer = _Trait<_Ty>::pointer;
using const_pointer = _Trait<_Ty>::const_pointer;
using difference_type = _Trait<_Ty>::difference_type;
constexpr void Push(const_reference element);
constexpr void Push(rvalue_reference element);
private:
value_type* myBuffer;
};
자료형 별칭(Type Alias)18. 개념 (Concept)[편집]
<C++ 예제 보기> import <print>; import <type_traits>; template<typename T> concept integrals = std::is_integral_v<T>; template<typename T> void Print(T&& value) { std::println("정수가 아닌 값 {} 출력", std::forward<T>(value)); } // 후방에 선언되어 있어도 정수인 경우를 잘 걸러낼 수 있다. template<integrals T> void Print(T&& value) { std::println("정수 값 {} 출력", std::forward<T>(value)); } int main() { // (1) // C++11의 메타 함수를 사용한 static_assert static_assert(std::is_integral_v<bool>); static_assert(std::is_integral_v<int>); static_assert(std::is_integral_v<unsigned int>); static_assert(std::is_integral_v<long long>); static_assert(std::is_integral_v<float>); // 컴파일 오류! static_assert(std::is_integral_v<double>); // 컴파일 오류! // (2) // C++20의 제약 조건을 사용한 static_assert static_assert(integrals<bool>); static_assert(integrals<char>); static_assert(integrals<int>); static_assert(integrals<unsigned int>); static_assert(integrals<long long>); static_assert(integrals<float>); // 컴파일 오류! static_assert(integrals<double>); // 컴파일 오류! // (3) // 템플릿에서 제약 조건 사용 // (3-1) // "정수 값 10000 출력" Print(10'000); // (3-2) // "정수 값 10000000 출력" Print(10'000'000ULL); // (3-3) // "정수가 아닌 값 Hello, world! 출력" Print("Hello, world!"); // (3-4) // "정수가 아닌 값 nullptr 출력" Print(nullptr); }
"display: none; display: 문단=inline"를 참고하십시오.
19. 특성 (Attribute)[편집]
"display: none; display: 문단=inline"를 참고하십시오.
이 문서의 내용 중 전체 또는 일부는 2024-10-02 05:56:09에 나무위키 C++/문법 문서에서 가져왔습니다.
[1] bool은 논리 자료형을 의미하는 'Boolean'의 줄임말이다.[호출] 연산자[2] 이를 인라이닝(Inlining)이라고 한다[3] 예를 들어 extern 함수 또는 모듈에서 export된 함수[헤더] A B [4] 그러나 C++14 이하의 버전을 사용해야만 하는데 문자열 처리를 구현해야 하면 공부해서 나쁠 건 없다.[5] MSVC[6] 예를 들어 if (handle == NULL) 같은 경우[7] 이러면 주소를 얻을 수 있고 값을 바꿀 수는 있겠지만 성능상의 이득은 사라진다.[8] 여기서 기본 생성자는 default가 아니여도 된다[9] extern "C" { ...; }와 같이 스코프를 지정할 수 있다[10] 연산자 오버로딩은 가능하다[11] 전역 이름공간도 포함[12] 현재는 Zig 정도가 상수 표현식 기능을 제공한다