10.5 C++11 / C++14
C++11과 C++14에서는 여러 가지 새로운 기능이 도입되었다. 이 절에서는 다른 장들에서 설명하기에 적합하지 않은 C++11과 C++14의 새로운 기능을 설명한다.
10.5.1 유니폼 초기화
C++11이전에는 데이터 타입에 대한 초기화 방식에 일관성이 없었다.
예를 들어 다음의 Circle 데이터에 대한 정의를 보자. 하나는 구조체를 이용하고 다른 하나는 클래스를 이용하고 있다.
struct CircleStruct
{
int x, y;
double radius;
};
class CircleClass
{
public:
CircleClass(int x, int y, double radius)
: mX(x), mY(y), mRadius(radius) {}
private:
int mX, mY;
double mRadius;
};
C++11 이전에는 CircleStruct변수와 CircleClass변수에 대한 초기화 방법이 다음과 같이 서로 달랐다.
CircleStruct myCircle1 = {10, 10, 2, 5};
CircleClass myCircle1(10, 10, 2, 5);
구조체 변수에 대해서는 { ... }를 사용하고 클래스 변수에 대해서는 생성자를 함수로 호출하는 것 처럼 (...)를 사용한다.
C++11부터는 일관성 있게 클래스 초기화에도 { ... }를 이용할 수 있다. 위 코드는 다음처럼 일관성 있는 형태로 바꿀 수 있다.
CircleStruct myCircle1 = {10, 10, 2, 5};
CircleClass myCircle1 = {10, 10, 2, 5};
변수 myCircle4를 선언하면 자동으로 CircleClass의 생성자가 호출된다. 그리고 여기에 사용된 =는 다음처럼 생략할 수 있다.
CircleStruct myCircle1{10, 10, 2, 5};
CircleClass myCircle1{10, 10, 2, 5};
유니폼 초기화를 구조체나 클래스 외에도 사용할 수 있다. 무엇이든 초기화가 필요하다면 사용할 수 있다. 예를 들어 아래 코드는 동일하게 변수를 3으로 초기화한다.
int a = 3;
int b(3);
int c = {3}; // 유니폼 초기화
int d{3}; // 유니폼 초기화
0 으로 초기화 할 경우 유니폼 초기화의 인자를 생략할 수 있다. 아래 코드는 e의 값을 0으로 초기화한다.
int e{}; // 유니폼 초기화 e의 값이 0이 된다.
유니폼 초기화를 이용하면 축소 변환(narrowing)을 방지할 수 있다. C++에서는 다음과 같이 데이터에 대한 축소 변환이 암묵적으로 수행된다.
void func(int i) { /* ... */ }
int main()
{
int x = 3.14;
func(3.14);
return 0;
}
C++는 3.14를 변수 x에 대입하거나 함수 func()의 인자로 전달할 떄 자동으로 int타입의 표현 범위에 맞도록 데이터 값을 3으로 축소 변환한다.
어떤 컴파일러는 축소 변환이 일어나면 경고 메시지를 출력한다. 하지만 다음과 같이 유니폼 초기화를 사용할 수도 있다.
void func(int i) { /* ... */ }
int main()
{
int x = 3.14;
func(3.14);
return 0;
}
C++는 3.14를 변수 x에 대입하거나 함수 func()의 인자로 전달할 떄 자동으로 int타입의 표현 범위에 맞도록 데이터 값을 3으로 축소 변환한다. 어떤 컴파일러는 축소 변환이 일어나면 경고 메시지를 출력한다. 하지만 다음과 같이 유니폼 초기화를 사용할 수도 있다.
void func(int i) { /* ... */ }
int main()
{
int x = {3.14}; // 축소 변환 떄문에 컴파일러 경고 또는 에러 발생
func({3.14}); // 축소 변환 때문에 컴파일러 경고 또는 에러 발생
return 0;
}
C++11 표준을 준수하는 컴파일러라면 대입과 함수 호출 두 경우 모두에서 축소 변환이 필요한 유니폼 초기화에서 컴파일 에러를 발생시킨다.
유니폼 초기화는 STL컨테이너에도 이용할 수 있다. 예를 들어 string의 vector를 초기화 할 때 유니폼 초기화가 지원되지 않으면 다음과 같이 해야한다.
std::vector<std::string> myVec;
myVec.push_back("String 1");
myVec.push_back("String 2");
myVec.push_back("String 3");
하지만 유니폼 초기화를 이용하면 같은 작업을 다음과 같이 더 간결하게 할 수 있다.
std::vector<std::string> myVec = {"String 1", "String 2", "String 3"};
유니폼 초기화는 동적으로 할당되는 배열에도 사용할 수 있다.
int* pArray = new int[4]{0, 1, 2, 3};
마지막으로, 생성자 초기화 리스트에서 멤버 배열을 초기화하는 데도 편리하게 사용할 수 있다.
class MyCless
{
public:
myClass() : mArray{0, 1, 2, 3} {}
private:
int mArray[4];
};
10.5.2 초기화 리스트
초기화 리스트는 <initializer_lsit> 헤더 파일에 정의되어 있고, 파라미터 개수가 가변적인 함수를 작성할 때 편리하게 이용할 수 있다. C에서부터 사용한 기존의 가변 인자 리스트(va_list, va_start, va_end)와 달리 C++11의 초기화 리스트에는 한 가지 타입만 사용할 수 있다. 초기화 리스트의 사용 방법은 다음 예제에서 쉽게 알 수 있다.
#include <initializer_list>
using namespace std;
int makeSum(initializer_list<int> lst)
{
int total = 0;
for ( const auto& value : lst ) {
total += value;
}
return total;
}
makeSum() 함수는 초기화 리스트를 통해 int 인자를 받는다. 이 함수 안에서는 반복자 루프를 이용해서 리스트의 전체 항목을 더하여 결과를 만들어낸다. 이 함수는 다음과 같이 사용될 수 있다.
int a = makeSum({1, 2, 3});
int b = makeSUm({10, 20, 30, 40, 50, 60});
초기화 리스트는 어떤 타입이 허용될지 리스트에서 정해놓기 때문에 타입 세이프하다. 예를 들어 위 makeSum()함수는 double타입 값을 이용할 수 없다.
int c = makeSum({1, 2, 3, 0});
마지막 인자 항목이 double이기 때문에 컴파일 에러 또는 경고가 발생한다.
10.5.3 명시적 변환 연산자
컴파일러에 의한 의도하지 않은 자동 변환을 막기 위해 explicit 키워드를 사용하는 방법이 있다. C++컴파일러는 커스텀 변환 연산자가 구현되어 있을 때도 묵시적으로 타입 변환을 수행한다.'
C++11 버전 이후부터는 생성자 뿐만 아니라 변환 연산자에도 explicit 키워드를 적용할 수 있다.
명시적 변환 연산자를 배우기 전에 묵시적 변환에 대해 먼저 이해해야 한다. 다음 코드를 보자. IntWrapper 클래스는 int타입을 감싸서 int() 변환 연산자를 구현하고 있다. 컴파일러는 이 변환 연산자를 이용해서 IntWrapper를 int로 변환할 수 있다.
class IntWrapper
{
public:
IntWrapper(int i) : mInt(i) {}
operator int() const { return mInt; }
private:
int mInt;
};
다음 코드는 묵시적 변환의 예다. iC1은 값 123을 가지게 된다.
IntWrapper c(123);
int iC1 = c;
다음처럼 변환 연산자 int()가 명시적으로 사용되도록 컴파일러에 알려줄 수도 있다.
int iC2 = static_cast<int>(c);
C++11부터는 다음처럼 explicit 키워드를 이용해서 컴파일러의 묵시적 변환을 막을 수 있다.
class IntWrapper
{
public:
IntWrapper(int i) : mInt(i) {}
explicit operator int() const { return mInt; }
private:
int mInt;
};
위와 같이 int()변환 연산자가 명시적(explicit)으로 사용되도록 제한해놓으면 다음과 같이 묵시적 변환에 의존하는 코드는 컴파일 에러가 발생한다.
IntWrapper c(123);
int iC1 = c; // int()변환 연산자가 explicit로 선언되어 있어 컴파일 에러 발생
변환 연산자를 explicit으로 정의하였으면 다음처럼 명시적으로 호출해야 한다.
int iC2 = static_cast<int>(c);
10.5.4 속성
C++11에서는 소스 코드에 벤더 종속적인 정보나 옵션 사항을 지정해야 할 때 속성(attributes)을 이용할 수 있다. 기존에는 __attribute__, __declspec 등과 같이 비표준적인 방법으로 특정 환경에만 유효한 정보를 설정했다. C++11부터는 이중 대괄호를 이용해서 [[attribute]]와 같은 문법으로 속성값을 설정한다.
C++11에서는 두 개의 속성 [[noreturn]]과 [[carries_dependency]]만 표준으로 정의했으나 C++14에서는 [[deprecated]]속성도 추가했다.
[[noreturn]]은 함수가 호출된 지점으로 절대 리턴하지 않는다는 것을 의미한다. 이런 종류의 함수는 보통 프로그램이나 스레드를 종료시켜버리거나 익셉션을 발생시킨다. 다음은 그러한 예다.
[[noreturn]] void func()
{
throw 1;
}
두 번째 속성 [[carries_dependency]]는 아주 특수한 경우에만 사용되기 때문에 여기서는 설명하지 않는다.
[[deprecated]]속성은 당장은 사용할 수 있지만, 앞으로 사용 중단 안내될 기능임을 표시하여, 다른 대체 기능을 사용하도록 유도하는 데 사용된다. 이 속성을 지정할 때는 사용 중단 예정인 사유를 주석으로 추가할 수도 있다.
예를 들면 [[deprecated("안전하지 않은 메서드, xyz를 사용하시오")]] 와 같이 할 수 있다.
대부분의 속성은 벤더 종속적이다. C++표준에서는 속성을 프로그램의 의미를 바꾸는 데 이용하지 않도록 권고하고 있다. 단, 코드를 최적화 하거나 오류를 찾아내는 데 도움을 주는 용도로 사용하는 것을 권장한다. 속성값이 벤더별로 충돌할 수 있기 때문에 다음과 같이 벤더명으로 속성값을 구분할 것도 권고하고 있다.
[[clang::noduplicate]]
10.5.5 사용자 정의 리터럴
C++에서는 다음과 같이 몇 가지 리터럴 표준이 제공된다.
- 'a' : 문자
- "character array" : 0값으로 종료되는 문자 배열. C 스타일 문자열
- 3.14f : flaot 부동 소수점값
- 0xabc : 16진 숫자값
C++11부터는 프로그래머가 자신만의 리터럴을 정의할 수 있게 해준다. 사용자 정의 리터럴은 밑줄로 시작하고, 리터럴 연산자를 구현함으로써 사용할 수 있다. 리터럴 오퍼레이터는 미가공모드(raw mode)와 가공모드(cooked mode) 가 있다. 미가공 모드에서는 리터럴 연산자가 일련의 문자를 받게 되고 가공 모드에서는 해석된 특정 타입의 값을 받게 된다.
예를 들어 C++리터럴 123이 있다고 할 때 미가공 모드에서는 리터럴 연산자가 문자열 '1' , '2', '3'을 받고, 가공 모드에서는 int 타입 값 123을 받게 된다. 또 다른 예로 C++리터럴은 0x23은 미가공 모드에서는 '0', 'x', '2', '3' 을, 가공 모드에서는 int타입값 35를 받는다. 또한 C++리터럴 3.14는 미가공 모드에서는 '3', '.', '1', '4' 를, 가공 모드에서는 부동 소수점값 3.14를 받는다.
가공모드 리터럴 연산자는 다음 조건을 갖추어야 한다.
- 숫자값을 처리하려면 unsinged long long, long double, char, wchar_t, char16_t, char32_t 중 하나를 파라미터 타입으로 이용한다.
- 또는 문자 배열과 그 배열의 크기를 파라미터로 선언하여 문자열을 처리해야 한다. 예를 들어 'const char* str, size_t len' 과 같이 파라미터를 선언한다.
다음은 가공 코드 리터럴 연산자의 구현 예다. 이 연산자는 _i 리터럴을 이용해서 복소수를 정의한다.
std::complex<double> operator"" _i(long double d)
{
return std::complex<double>(0, d);
}
_i 리터럴은 다음과 같이 사용될 수 있다.
std::complex<double> c1 = 9.634_1;
auto c2 = 123_i; // c2는 std::complex<double> 타입을 가진다.
다음 코드는 가공 모드 리터럴 연산자의 구현 예로 _s 리터럴을 str::string 데이터 타입으로 정의한다.
std::string operator:: _s(const char* std, size_t len)
{
return std::string(str, len);
}
이 리터럴은 다음과 같이 사용될 수 있다.
std::strign str1 = "Hello World"_s;
auto str2 = "Hello World"_s; //str2 는 std::string 타입을 가진다.
만약 _s리터럴을 사용하지 않으면 auto 타입의 변환 룰에 의해 const char* 로 해석된다.
auto str3 = "Hello World"; // str3은 const char* 타입을 가진다.
미가공 모드 리터럴 연산자는 const char* 타입 또는 null로 끝나는 C스타일 문자열 파라미터를 가진다.
다음은 _i 리터럴을 미가공 모드로 정의하는 예다.
std::complex<double> operator"" _i(const char* p) {}
미가공 모드 리터럴 연산자는 가공 모드 리터럴 연산자와 사용 방법이 같다.
10.5.5.1 표준 사용자 정의 리터럴
C++14 에서는 아래와 같은 표준 사용자 정의 리터럴을 지원하고 있다.
- 's' 첨자는 std::string 타입 리터럴을 의미한다. 예를 들면 아래의 auto 변수는 std::string으로 취급된다.
auto myString = "Hello World"s; - 'h', 'min', 's', 'ms', 'us', 'ns'는 19장에서 설명하는 날짜 데이터 타입 std::chrono::duration의 시간간격을 의미한다. 예를 들면 아래와 같이 쓰인다.
auto myDuration = 42min; - 'i', 'il', 'if'는 각각 복소수 데이터 타입 complex<double>, complex<long double>, complex<float>를 의미한다. 예를 들면 아래와 같이 쓰인다.
auto myComplexNumber = 1.3i;
'전문가를 위한 C++정리' 카테고리의 다른 글
10. C++의 까다롭고 유별난 부분들 10.6 헤더 파일 (0) | 2024.03.19 |
---|---|
10. C++의 까다롭고 유별난 부분들 10.4 스코프 지정 (0) | 2024.03.11 |
10. C++의 까다롭고 유별난 부분들 10.3 타입과 캐스팅 (0) | 2024.03.08 |
10. C++의 까다롭고 유별난 부분들 10.2 키워드 혼동 (0) | 2024.03.07 |
10. C++의 까다롭고 유별난 부분들 10.1 참조형 (0) | 2024.03.05 |