프로그래밍 공부
작성일
2024. 3. 14. 15:11
작성자
WDmil
728x90

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;
728x90