💬 C++ Core Guidelines에서는 '0' 혹은 'NULL'보다는 'nullptr'를 사용하라고 권장하고 있는데, 권장하기에 사용하기보다는 왜 nullptr를 쓰는 게 나은지 다양한 방법으로 알아보자.
nullptr를 써야 하는 이유
널포인터 대신 0을 사용하면서 오류가 발생한다.
사례 1
Example
int main()
{
int* p1 = 0;
int* p2 = nullptr;
}
위 코드는 문제없이 p1, p2에 널 포인트가 대입이 된다. 다음 예제도 문제가 없는지 검토해 보자.
Example
void foo(int* p) {}
int main()
{
int n = 0;
foo(0); //1 ??
foo(n); //2 ??
}
주석의 결과를 확인해 보면 다음과 같다.
주석 1 : OK - 0이 int 타입입니다. '리터럴 0'은 정수이지만 포인터로 암시적 형변환이 된다.
주석 2 : Error - 0을 가진 '정수형 변수 n'은 포인터로 변환될 수 없기 때문이다.
- 리터럴에 대한 설명은 글 맨 아래 참조에 정리하였다.
사례 2
Example
nullptr 대신 리터럴 '0'를 사용했을 때 문제가 되는 예를 하나 더 확인해 보자.
아래 코드에 주석 1, 2의 결과와 그 이유를 생각해 보자.
#include<iostream>
template<typename F, typename T>
void forward_arg(F f, T&& arg)
{
f(std::forward<T>(arg));
}
void foo(int* p) {}
int main()
{
int n = 0;
foo(0); // ok
//foo(n); // error
forward_arg(foo, 0); // 1. result ??
forward_arg(foo, nullptr); // 2. result ??
}
📌 우선 perfect forwarding을 모르면 아래 글을 우선 보고 오자.
- 💡 std::forward를 모르지만 위 글을 읽지 않고 이어서 읽고 싶으면 std::forward(arg)는 static_cast<T&&>(arg) 이렇게 해석하면 된다.
주석의 결과와 이유를 확인해 보면 다음과 같다.
주석 1 : Error
void forward_arg(F f, T&& arg) // '리터럴 0'이 넘어오면
T&& arg => int&& arg = 0가 되면서 arg는 0을 가진 변수가 되고, f 함수는 아래와 같이 된다.
f(std::forward<T>(arg)) == f(std::forward<int>(arg)) == f(static_cast<int&&>(arg))
: arg를 l-value -> r-value로 속성을 변경했지만 arg는 int 타입이기에 아래와 같이 타입이 다르다는 에러가 발생한다.
invalid conversion from 'int' to 'int*' [-fpermissive]
주석 2 : OK
void forward_arg(F f, T&& arg) // nullptr이 넘어오면
T&& arg => nullptr_t&& arg = nullptr 가 되면서 f 함수는 아래와 같이 된다.
f(std::forward<T>(arg)) => f(std::forward<nullptr_t>(arg)) => f(static_cast<nullptr_t&&>(arg))
결국 이 코드는 foo(nullptr_t arg)가 되는데 nullptr_t가 포인터로 암시적 형변환이 되어 정상동작이 된다.
💬 nullptr_t가 왜 포인터로 형변환 되는지 확인하기 이전에 사례를 하나만 더 확인해 보자.
사례 3
Example
forwarding argument를 직접적으로 예를 들었지만 이를 사용한 예시가 바로 thread이다. 아래 예제를 보자.
#include <iostream>
#include <thread>
void foo(int* p) {}
int main()
{
// std::thread t(&foo, 0); //1. Error
std::thread t(&foo, nullptr); // Ok..!!
t.join();
}
주석의 결과와 이유를 확인해 보면 다음과 같다.
주석 1 : Error
std::thread t(&foo, 0);
thread 생성자가 &foo, 0을 받게 되면 새로운 스레드를 생성해서 "foo(0)"을 실행하게 되는데 인자를 전달(forward) 하기 때문에 foo(arg)와 같은 에러가 발생하게 된다.
🚀 nullptr의 출생의 비밀을 알아보자.
C++11 이전에는 nullptr를 아래처럼 정의해 썼었다고 한다.
Code
struct nullptr_t
{
operator int*() const { return 0; }
operator char*() const { return 0; }
...
operator xxx const { return 0; }
}
nullptr_t mynullptr;
nullptr_t는 모든 타입의 포인터를 변환 연산자로 정의하여 암시적 형변환 시킨 형태의 구조체이다.
📌 '변환 연산자'가 생소한 사람은 아래 글을 먼저 이해하자.
사용 동작 예를 한 번 알아보자.
Example
void f1(int* p) {}
void f2(char* p) {}
int main()
{
nullptr_t mynullptr;
f1(mynullptr); //1
f2(mynullptr); //2
}
주석의 내부적 동작이 어떻게 되는지 확인해 보면 다음과 같다.
주석 1 : mynullptr => int* 변환 필요, mynullptr.operator int*()로 변환
주석 2 : mynullptr => char* 변환 필요, 아래처럼 동작 mynullptr.operator char*()로 변환
💬 앞선 예제에서 nullptr_t 타입의 변수가 포인터로 형변환이 된 이유가 바로 여기에 있다.
📜 C++11부터 표준에 포함되면서 "nullptr"는 키워드가 되었고 동작 원리는 위 코드와 동일한 규칙이다.
💬 nullptr는 널포인트 사용을 위해 잘 설계된 키워드라는 설명이 충분히 되었으면 좋겠다.
참고
리터럴
literal은 변수가 아닌 프로그램에서 직접 사용하는 값이다. 그리고 모든 리터럴은 타입이 있기에 아래 예제에서 각각의 타입을 생각해 보자.
Example
#include<cstddef>
int main()
{
3; // 1
3.4; // 2
true; // 3
nullptr; // 4
std::nullptr_t a = nullptr; // 5
int* p1 = a;
char* p2 = a;
}
- 주석 1 : Int 타입
- 주석 2 : double 타입
- 주석 3 : bool 타입 - true는 값이고 컴파일가 인식하는 키워드이다.
- 주석 4 : std::nullptr_t 타입 - nullptr도 값이고 컴파일러가 인식하는 키워드이다.
- 주석 5 : 리터럴도 타입이 있기에 a처럼 nullptr 대신 사용 할 수도 있다.
C++ 핵심 가이드라인 한글화 프로젝트 링크
C++ Core Guidelines 원문 링크

'L C++' 카테고리의 다른 글
trivial 뜻 : 하찮은, 그렇지만 하찮지 않은 trivial‼ (0) | 2024.08.25 |
---|---|
컴파일러와 페어 프로그래밍 - 특별 멤버 함수 편 🚀 (0) | 2024.08.15 |
new/delete 동작 원리 이해하기 (4) | 2024.07.22 |
내가 설계한 객체에 유효성 기능 지원하기 (2) | 2024.06.09 |
애증의 const 사용하기 (2) (49) | 2024.05.26 |