취미/개발서적

전문가를 위한 C++ - Chapter2

게임 개발 2024. 5. 8. 14:45

프로그램을 작성하다 보면 스트링을 사용할 일이 생기기 마련이다.
C언어를 사용하던 시절에는 단순히 널null 로 끝나는 문자 배열로 스트링을 표현했다.
하지만 이렇게 하면 버퍼 오버플로를 비롯한 다양한 문제 때문에 취약점이 드러날 수 있다.
C++ 표준 라이브러리는 이러한 문제를 방지하기 위해 안전하고 사용하기 쉬운 std::string 클래스를 제공한다.
스트링은 굉장히 중요한 기능이므로 자세히 알아보자.
 

동적 스트링

스트링을 주요 객체로 제공하는 프로그래밍 언어를 보면 대체로 스트링의 크기를 임의로 확장하거나,
서브스트릥(부분 문자열)을 추출하거나 교체하는 것처럼 고급 기능을 제공한다.
반면 C와 같은 언어는 스트링을 부가 기능처럼 취급한다.
그래서 스트링을 언어의 정식 데이터 타입으로 제공하지 않고
단순히 고정된 크기의 바이트 배열로 처리했다.
이러한 언어에서 제공하는 스트링 라이브러리를 보면 경곗값 검사와 같은
기능조차 없이 기본적인 함수 구현만으로 구성되어 있다.
반면 C++는 스트링을 핵심 데이터 타입으로 제공한다.
 

C스타일 스트링

C언어는 스트링을 문자 배열로 표현했다.
스트링의 마지막에 널 문자(\0)를 붙여서 스트링이 끝났음을 표현했다.
이러한 널 문자에 대한 공식 기호는 NUL이다.
여기서는 L이 두 개가 아니라 하나며 NULL 포인터와는 다른 값이다.
C++에서 제공하는 스트링이 훨씬 뛰어나지만 
C언어에서 스트링을 다루는 방법도 알아둘 필요가 있다.
아직도 C스타일 스트링을 쓰는 C++ 프로그램이 많기 때문이다.
대표적인 예로 인터페이스를 C스타일로 제공하는 외부 라이브러리나 운영체제와 연동하는 C++코드를 들 수 있다.
 
C 스트링을 다룰 때 \0 문자를 담을 공간을 깜빡하고 할당하지 않는 실수를 저지르기 쉽다.
예를 들어 'hello'만 스트링을 구성하는 문자는 다섯 개이지만,
메모리에 저장할 때는 문자 여섯개만큼 공간이 필요하다.
 
C++에서는 C언어에서 사용하던 스트링 연산에 대한 함수도 제공한다.
이러한 함수는 <cstring>헤더 파일에 정의돼 있다.
이런 함수는 대체로 메모리 할당 기능을 제공하지 않는다.
예를 들어 strcpy() 함수는 스트링 타입 매개변수를 두 개 받아서 두 번째 스트링을 첫 번째 스트링에 복사한다.
이때 두 스트링의 길이가 같은지 확인하지 않는다.
다음 코드는 이미 메모리를 할당된 스트링을 매개변수로 받지 않고,
주어진 스트링에 딱 맞게 메모리를 할당한 결과를 리턴하는 함수를
strcpy()에 대한 래퍼 함수 형태로 구현한 예를 보여주고 있다.
여기서 스트링의 길이는 strlen() 함수로 구한다.
copyString()에서 할당한 메모리는 이 함수를 호출한 측에서 해제해야 한다.
 

char* copyString(const char* str)
{
	char* result = new char[strlen(str)]; // 버그! 한 칸 부족하다.
	strcpy(result, str);
	return result;
}

 
위 copyString() 함수 코드에 오류가 하나 있다.
strlen() 함수에서 리턴하는 값은 스트링을 저장하는 데 사용된 메모리 크기가 아니라 
스트링 길이 함수라는 점이다.
따라서 strlen()은 'hello'란 스트링에 대해 6이 아닌 5를 리턴한다.
따라서 스트링을 저장하는 데 필요한 메모리를 제대로 할당하려면 문자 수에 1을 더한 크기로 지정해야 한다.
항상 +1이 붙어서 지저분하지만 어쩔수 없다.
C스타일의 스트링을 다룰 때는 항상 이 점을 명심해야 한다.
위 함수를 제대로 작성하면 다음과 같다.
 

char* copyString(const char* str)
{
	char* result = new char[strlen(str) + 1];
	strcpy(result, str);
	return result;
}

 
여러 스트링을 조합해서 만든 스트링을 할당하는 경우를 생각해보면
왜 strlen() 함수가 스트링에 담긴 실제 문자 수만 리턴하도록 구현됐는지 이해할 수 있다.
예를 들어 인수로 받은 스트링 세 개를 하나로 합쳐서 리턴하는 함수를 생각해보자.
이때 결과로 나오는 스트링에 대한 메모리 공간은 얼마가 필요할까?
스트링에 딱 맞게 공간을 할당하려면 세 스트링의 길이를 strlen() 함수로 구해서
모두 더한 값에 마지막 '\0' 문자에 대한 공간 하나를 추가해야 한다.
만약 strlen() 함수가 '\0'을 포함한 기링를 리턴하도록 구현됐다면 메모리 공간에 딱 맞게 게산하기 번거롭다.
다음 코드는 방금 설명한 작업을 strcpy()와 strcat()함수로 처리하는 예를 보여주고 있다.
참고로 strcat() 에서 cat은 concatenate에서 따온것이다.
 
 

char* appendStrings(const char* str1, const char* str2, const char* str3)
{
	char* result = new char[strlen(str1) + strlen(str2) + strlen(str3) + 1];
	strcpy(result, str1);
	strcpy(result, str2);
	strcpy(result, str3);
	return result;
}

 
 
C와 C++에서 제공하는 sizeof() 연산자는 데이터 타입이나 변수의 크기를 구하는 데 사용된다.
예를 들어 sizeof(char)는 1을 리턴하는데, char의 크기가 1바이트이기 때문이다.
하지만 C스타일 스트링에 적용할 때는 sizeof()와 strlen()의 결과가 전혀 다르다.
따라서 스트링의 길이를 구할 때는 절대로 sizeof()와 strlen()의 결과가 전혀 다르다.
따라서 스트링의 길이를 구할 때는 절대로 sizeof()를 사용하면 안 된다.
예를 들어 다음과 같이 스트링을 char[]로 저장하면 sizeof()는 '\0'을 포함하여
그 스트링에 대해 실제로 할당된 메모리 크기를 리턴한다.
 

char text1[] = "abcdef";
size_t s1 = sizeof(text1); //7
size_t s2 = strlen(text1); //6

 
 
반면 C 스타일 스트링을 char*로 저장했다면 sizeof()는 포인터의 크기를 리턴한다.
 

int main(void)
{
	
	const char* text2 = "abcdef";
	size_t s3 = sizeof(text2); // 플렛폼마다 다름 64에서는 8 84에서는 4
	size_t s4 = strlen(text2);	// 6

	cout << s3 << "      " << s4;


	return 0;

}

 
이 코드를 32비트 모드에서 컴파일하면 s3의 값은 4고, 64비트 모드에서 컴파일하면 8이다.
sizeof()가 포인터 타입인 const char* 의 크기를 리턴하기 때문이다.
 
 

2.1.2스트링 리터럴

 
C++ 프로그램에서 스트링을 인용부호로 묶은 것을 본 적이 있을 것이다.
예를 들어 다음 코드는 hello란 스트링을 변수에 담지 않고 스트링값을 곧바로 화면에 출력한다.
 
이처럼 곧바로 나오게 되는 hello처럼
변수에 담지 않고 곧바로 값으로 표현한 스트링을 스트링 리터럴이라고 부른다.
스트링 리터럴은 내부적으로 메모리의 읽기 전용 영역에 저장된다.
그래서 컴파일러는 같은 스트링 리터럴이 코드에 여러 번 나오면
그중 한 스트링에 대한 레퍼런스를 재사용하는 방식으로 메모리를 절약한다.
다시 말해 코드에서 'hello'란 스트링 리터럴을 500번 넘게 작성해도
컴파일러는 hello에 대한 메모리 공간을 딱 하나만 할당한다.
이를 리터럴 풀링(literal pooling)이라 한다.
 
스트링 리터럴을 변수에 대입할 수는 있지만, 메모리의 읽기 전용 영역에 있게 되거나
동알한 리터럴을 여러 곳에서 공유할 수 있기 때문에 변수에 저장하면 위험하다.
C++ 표준에서 스트링 리터럴을 'const char가 n개인 배열'타입으로 정의하고 있다.
하지만 const가 없던 시절에 작성된 레거시 코드의 하위 호완성을 보장하도록
스트링 리터럴을 const char*가 아닌 타입으로 저장하는 컴파일러도 많다.
const 없이 char* 타입 변수에 스티링 리터럴을 대입하더라도
그 값을 변경하지 않는 한 프로그램 실행에는 아무런 문제없다.
스트링 리터럴을 수정하는 동작에 대해서는 명확히 정의돼 있지 않다.
따라서 프로그램이 갑자기 죽을 수도 있고,
실행은 되지만 겉으로 드러나지 않는 효과가 발생할 수도 있고,
수정 작업을 그냥 무시할 수도 있고, 의도한 대로 작동할 수도 있다.
구체적인 동작은 컴파일러마다 다르다.
예를 들어 다음과 같이 코드를 작성하면 결과를 예측할 수 없다.
 

char* ptr = "hello"; 	//변수에 스티링 리터럴을 대입한다.
ptr[1] = 'a'; //결과를 예측할 수 없다.

 
스트링 리터럴을 참조할 때는 const 문자에 대한 포인터를 사용하는 것이 훨씬 안전하다.
다음 코드도 위와 똑같은 버그를 담고 있지만
스트링 리터럴을 const char* 타입 변수에 대입했기 때문에
컴파일러는 읽기 전용 메모리에 쓰기 작업을 실행하는 것을 거를 수 있다.
 

const char* ptr = "hello"; 		//변수에 스트링 리터를을 대입한다.
ptr[1] = 'a';				//읽기 전용 메모리에 값을 쓰기 때문에 에러가 발생한다.

 
문자 배열 (char[])의 초깃값을 설정할 때도 스트링 리터럴을 사용한다.
이때 컴파일러는 주어진 스트링을 충분히 담을 정도로
큰 배열을 생성한 뒤 여기에 실제 스트링값을 복사한다.
컴파일러는 이렇게 만든 스트링 리터럴을
읽기 전용 메모리에 넣지 않으며 재사용하지 않아도 된다.
 

char arr[] = "hello";		//컴파일러는 적절한 크기의 문자 배열 arr을 생성한다.
arr[1] = 'a';		//이제 스트링을 수정할 수 있다.

 
 

로 스티링 리터럴 (raw string literal)

 
로 스트링 리터럴이란 여러 줄에 걸쳐 작성한 스트링 리터럴로서,
그 안에 담긴 잉용 부호를 이스케이프 시퀀스로 표현할 필요가 없고,
\t나 \n 같은 이스케이프 시퀀스를 일반 텍스트로 취급한다.
예를 들어 일반 스트링 리터럴을 다음과 같이 작서아혐ㄴ 스트링 안에 있는
큰따옴표를 이스케이프 시퀀스로 표현하지 않았기 때문에 컴파일 에러가 발생한다.
 

const char* str = "Hello World"!"; // 에러 발생

 
이럴 때는 큰따옴표를 다음과 같이 이스케이프 시퀀스로 표현한다.
 

const char* str = "Hello \"World\"!";

 
하지만 로 스트링 리터럴을 사용하면 인용부호를 이스케이프 시퀀스로 표현하지 않아도 된다.
로 스티링 리터럴은 R"(로 시작해서 )"로 끝난다.
 
로 스트링 리터럴을 사용하지 않고 여러 줄에 걸친 스트링을 표현하려면
스트링 안에서 줄이 바뀌는 지점에 \n을 넣어야 한다.
예를 들면 다음과 같다.
 

const char* str = "Line 1\nLine 2";

 
이 스트링을 콘솔에 출력하면 다음과 같은 결과가 나온다.
 

 
로 스트링 리터럴로 표현할 때는 다음과 같이 소스 코드에서 줄바꿈을 할 지점에
\n 이스케이프 시퀀스를 입력하지 말고 그냥 엔터키를 누르면 된다.
그러면 앞에서 \n을 지정했을 때와 똑같이 출력된다.
 

const char* str = R"(Line 1
Line 2)";

 
로 스트링 리터럴에서는 이스케이프 시퀀스를 무시한다.
예를 드렁 다음과 같이 로 스트링 리터럴을 작성하면 \t 이스케이프 시퀀스가
탭 문자로 바뀌지 않고 백슬래시 뒤에 t라는 문자가 나온 것으로 표현한다.
 

	const char* str = R"(Is the following a tab character \t)";

 
따라서 이 스크링을 콘솔에 출력하면 위와 같이 나온다.
 
로 스트링 리터럴은 )"로 끝나기 때문에 그 안에 )"를 넣을 수 없다.
예를 들어 다음과 같이 )"가 중간에 들어가면 에러가 발생한다.
 

	
	const char* str = R"(Embedded)" character)"; // 에러가 발생

 
)" 문자를 추가하려면 다음과 같이
확정 로 스트링 리터럴 (extended raw literal)구문으로 표현해야 한다.
 

	R"d-char-sequence(r-char-sequence)d-char-sequence";

 
여기서 r-char-sequence에 해당하는 부분이 실제 로 스트링이다.
d-char-sequence라고 표현한 부분은 구분자 시퀀스(delimeter sequence)로서,
반드시 로 스트링 리터럴의 시작과 끝에 똑같이 나와야 한다.
이 구분자 시퀀스는 최대 16개의 문자를 가질 수 있다.
이때 구분자 시퀀스는 로 스트링 리터럴 안에 나오지 않는 값으로 지정해야 한다.
 
앞에 나온 스트링에서 고유한 구분자 시퀀스를 사용하도록 수정하면 다음과 같다.
 

	const char* str = R"-(Embedded )" characters)-";

 
 
로 스트링 리터럴을 사용하면 데이터베이스 쿼리 스트링이나
정규표현식, 파일 경로 등을 쉽게 표현할 수 있다.
 
 
2.1.3 C++std::string 클래스
 
C++ 표준 라이브러리는 스티링을 좀 더 잘 표현하도록 std::string 클래스를 제공한다.
엄밀히 말해 std::string은 basic_string이라는 클래스 템플릿의 인스턴스로서,
<cstring>의 함수와 기능은 비슷하지만 메모리 할당 작업을 처리해주는 기능이 더 들어있다.
string 클래스는 std 네임스페이스에 속하며 <string> 헤더에 정의되어 있다.
앞 장에서 이미 본 적 있지만 여기서 좀 더 자세히 알아보자.

 

1. C스타일 스트링의 문제점

 
C 스타일 스트링의 장단점을 살펴보면
C++의 string 클래스가 왜 필요한지 확실히 알 수 있다.
 
장점

  • 간단하다. 내부적으로 기본 문자 타입과 배열 구조체로 처리한다.
  • 가볍다. 제대로 사용하면 메모리를 꼭 필요한 만큼만 사용한다.
  • 로우 레벨이다. 따라서 메모리의 실제 상태를 조작하거나 복사하기 쉽다.
  • C 프로그래머에게 익숙하다. 새로 배울 필요가 없다.

 
단점

  • 스트링 데이터 타입에 대한 고차원 기능을 구현하려면
    상당한 노력이 필요하다.
  • 찾기 힘든 메모리 버그가 발생하기 쉽다.
  • C++의 객체지향적인 특성을 제대로 활용하지 못한다.
  • 프로그래머가 내부 표현 방식을 이해해야 한다.

 
위에서 나열한 장단점을 보면 이보다 더 나은 방식이 분명 있겠다는 생각이 들 것이다.
앞으로 배우겠지만 C++의string은 C 스타일의 스트링이 가진 장점은 그대로 유지하면서
단점을 해결해준다.
 

string 클래스 사용법

 
string은 실제로 클래스지만 마치 기본 타입인 것처럼 사용한다.
그래서 코드를 작성할 때는 기본 타입처럼 취급하면 된다.
C++ string에 연산자 오버로딩을 적용하면 C 스타일 스트링보다 휠씬 사용하기 편하다.
예를 들어 다음과 같이 + 연산자를 string에 적용하면
스트링 결합(연결) 연산을 수행하도록 정의할 수 있다.
다음 코드에서 변수 C값은 '1234'가 된다.
 

string A("12");
string B("34");
string C;
C = A + B; //C는 1234

 
마찬가지로 += 연산자가 스트링 뒤에 덧붙이는 연산(append)을 수행하도록 오버로딩 할 수 있다.
 

stirng A("12");
string B("34");
A += B; // A는 1234

 
 
C 스타일 스트링은 == 연산자로 비교할 수 없다는 단점이 있다.
예를 들어 다음과 같은 두 개의 스트링이 있다고 하자.

두 개의 스트링이 있다고 가정하면,
두 스트링을 비교하는 문장을 if( a== b)와 같이 작성하면
스트링 내용이 아닌 포인터 값을 비교하기 때문에 항상 false가 리턴된다.
C 언어에서 스트링을 비교하려면 if (strcmp(a,b) == 0) 과 같이 작정해야 한다.
또한 C 스티링은 <, <=, >=, >로 비교할 수 없기 때문에
주어진 스트링을 사전식 나열 순서에 따라 비교해서 -1,0 또는 1을 리턴하는 ctrcmp()를 사용했다.
따라서 코드가 지저분하고 읽기 힘들 뿐만 아니라 에러가 발생하기 쉽다.
 
C++에서 제공하는 string에서는 ==, !=, <와 같은 연산자를 오버로딩해서
스트링에 적용할 수 있다.
물론 C처럼 각각의 문자를 []로 접근할 수도 있다.
 
string을 확장해도 메모리 관련 작업은 C++에서는
string 클래스가 알아서 처리해준다는 것을 알 수 있다.
따라서 (허용된 범위를 벗어나는) 메모리 오버런이 발생할 걱정을 할 필요가 없다.
 
이러한 것을 기반으로 몇 가지 짚고 넘어갈 점이 있다.
첫째, 스트링을 할당하거나 크기를 조절하는 코드가 여러 군데 흩어져 있어도
메모리 누수가 발생하지 않는다는 것이다.
string 객체는 모두 스택 변수로 생성되기 때문이다.
string 클래스를 사용하면 메모리를 할당하거나 크기를 조절할 일이 상당히 많긴 하지만
string 객체가 스코프를 벗어나자마자 여기에 할당된 메모리를 string 소멸자가 모두 정리한다.
 
내 생각에는 스마트 포인터 처럼 스스로 생명주기를 조절하는 듯 하다.
즉 사용자가 할당 해제를 굳이 할 필요가 없다는 뜻으로 해석했다.
 
둘째, 연산자를 원하는 방식으로 작동하게 할 수 있다.
예를 들어 = 연산자를 스트링으로 복사하는 데 사용하면 상당히 편하다.
배열 방식으로 스트링을 다루는데 익숙하다면 이 기능이 굉장히 편하긴 하지만 좀 헷갈릴 수도 있다.
그래도 걱정할 필요는 없다.
일단 string 클래스의 처리 방식이 좋다는 것을 깨닫는 순간 금세 적응하게 된다.
 
string 클래스에서 제공하는 c_str()메서드를 사용하면
C 언어에 대한 호환성을 보장할 수 있다.
이 메서드는 C 스타일 스트링을 표현하는 const 문자 포인터를 리턴한다.
하지만 string에 대한 메모리를 다시 할당하거나 해당 string 객체를 제거하면
이 메서드가 리턴한 const 포인터를 더 이상 사용할 수 없게 된다.
따라서 현재 string에 담긴 내용을 정확히 사용하려면
이 메서드를 호출한 직후 리턴된 포인더를 활용하도록 코드를 작성하는 것이 좋다.
 
또한 함수 안에 생성된 스택 기반 string 객체에 대해서는 c_str()을 호출한 결과를
절대로 리턴값으로 전달하면 안 된다.
 
또한 string에서 제공하는 data() 메서드는 C++14까지만 해도 c_str()처럼
const char* 타입으로 값을 리턴했다.
그러나 C++17 부터는 non-const 스트링에 대해 호출하면 char*을 리턴하도록 변경했다.
 
string 객체를 다루기 위해 제공되는 연산의 전체 목록은 부록 B에서
표준 라이브러리 레퍼런스를 참고한다.
 

std::stirng리터럴

auto string1 = "Hello" // string1의 타입은 const char*이다.
auto string2 = "Hello"s; // string2의 타입은 std::string이다.

 
표준 사용자 정의 리터럴 's'를 사용하려면
using namespace std::string_literals; 또는 using namespace std;를 추가한다.
 
하이레벨 숫자 변환
std 네임스페이스는 숫자와 string을 쉽게 변환할 수 있도록
다양한 헬퍼(편의) 함수를 제공한다.
숫자 타입을 string으로 변환하는 함수는 다음과 같다.
이 함수들은 메모리 할당 작업도 처리해준다.
이 함수들은 string객체를 새로 생성해서 리턴한다.
 

이들 함수의 기능은 따로 설명하지 않아도 쉽게 이해할 수 있다.
위에 나온 value type을 string으로 변환하는 것이다.
 
이와 반대로 변환하는 함수도 다음과 같이 std 네임스페이스에 정의돼 있다.
이 함수의 프로토타입에서 str은 변환하려는 원본 string 값을 의미하고,
idx는 아직 변환되지 않은 부분의 맨 앞에 있는 문자의 인덱스를 가리키는 포인터고,
base 는 변환할 수의 밑(base, 기수, 기저)이다.
idx 포인터를 널 포인터로 지정하면 이 값을 무시한다.
여기 나온 변환 함수들은 제일 앞에 나온 공백 문자를 무시하고,
변환에 실패하면 invalid_argument 익셉션을 던지고,
변환된 값이 리턴 타입의 범위를 벗어나면 out_of_range 익셉션을 던진다.
 

 
예를 들면 다음과 같다.
 

이 코드의 실행 결과는 다음과 같다.
 

 
 
로우 레벨 숫자 변환
 
C++17부터 로우 레벨 숫자 변환에 대한 함수도 다양하게 제공된다.
이 함수는 <charconv>헤더에 정의돼 있다.
이 함수는 메모리 할당에 관련된 작업은 전혀 해주지 않기 때문에
호출한 측에서 버퍼를 할당하는 방식으로 사용해야 한다.
또한 고성능과 로케일 독립성에 튜닝됐다 (현지화 되었다.)
그래서 다른 하이 레벨 숫자 변환 함수에 비해 처리 속도가 엄청나게 빠르다.
숫자 데이터와 사람이 읽기 좋은 포멧 사이의 변환 작업을 로케일에 독립적이면서
빠른 속도로 처리하고 싶다면 이러한 로우 레벨 함수를 사용한다.
 
정수를 문자로 변환하려면 다음과 같은 함수를 사용한다.
 

여기서 IntegerT의 자리에 부호 있는 정수나, 부호 없는 정수 또는 char 타입이 나올 수 있다.
결과는 to_chars_result 타입으로 리턴되며, 다음과 같이 정의되어 있다.
 

 
정상적으로 변환됐다면 ptr 멤버는 끝에서 두 번쨰 문자를 가리키고,
그렇지 않다면 last 값과 같다 (이때 == errc::value_too_large다.)
 
예를 들면 다음과 같다

std::string out(10, ' ');
auto result = std::to_chars(out.data() + out.size(), 12345);
if (result.ec == std::errc()) {/* 제대로 반환된 경우 */}

 
1장에서 소개한 C++17의 구조적 바인딩을 적용하면 다음과 같이 표현할 수 있다.
 

 
또한 다음과 같이 부동 소수점 타입에 대한 변환 함수도 제공한다.

 
여기서 FloatT 자리에 float, double, long double이 나올 수 있다.
구체적인 포멧은 다음과 같이 정의된 chars_format 플래그를 조합해서 지정할 수 있다.
 

std::string_view 클래스

 
C++17 이전에는 읽기 전용 스트링을 받는 함수의 매개변수 타입을 쉽게 결정할 수 없었다.
const char*로 지정하면 std::string을 사용하는 클라이언트에서 c_str()나 data()를 이용하여
string을 const char*로 변환해서 호출해야 한다.
 
더 심각한 문제는 이렇게 하면 std::string의 객체지향 속성과
여기서 제공하는 뛰어난 헬퍼 메서드를 제대로 활용할 수 없다.
그렇다면 매개변수를 const std::string&로 지정하면 될까?
그렇게 하면 항상 std::string만 사용해야 한다.
 
예를 들어 스트링 리터럴을 전달하면 컴파일러는 그 스트링 리터럴의 복사본이 담긴
string 객체를 생성해서 함수로 전달하기 때문에 오버헤드가 발생한다.
간혹 이러한 함수에 대해 오버로딩 버전을 여러 개 만들기도 하는데
(예를 들어 어떤 버전은 const char*를 받고, 또 어떤 버전은 const string&을 받도록),
그리 세련된 방법은 아니다.
 
C++17부터 추가된 std::string_view 클래스를 사용하면 이러한 고민을 해결할 수 있다.
이 클래스는 std::basic_string_view 클래스 템플릿의 인스턴스로서
<string_view> 헤더에 정의돼 있다.
string_view는 실제로 const string& 대신 사용할 수 있으며 오버헤드도 없다.
다시 말해 스티링을 복사하지 않는다.
 
string_view의 인터페이스는 c_str()이 없다는 점을 제외하면
std::string과 같다. data()는 똑같이 제공된다.
string_view는 remove_prefix(size_t)와 remove_suffix(size_t)라는 메서드도 추가로 제공하는데,
지정한 오프셋만큼 스트링의 시작 포인터를 앞으로 당기거나 끝 포인터를
뒤로 미뤄서 스트링을 축소하는 기능을 제공한다.
 
참고로 string과 string_view를 서로 연결/결합할 수 없다.
예를 들어 다음과 같이 작성하면 컴파일 에러가 발생한다.
 

 
제대로 컴파일하면 마지막 줄을 다음과 같이 수정한다.
 

 
이처럼 std::string 을 사용할 줄 안다면 string_view의 사용법을 따로 배우지 않아도 곧바로 쓸 수 있다.
 
만약 extracExtension() 함수라는 사용한다고 생각해보자.
이 함수는 주어진 파일명에서 확장자만 뽑이서 리턴한다.
참고로 string_view는 대부분 값으로 전달을 한다.
스트링에 대한 포인터와 길이만 갖고 있어서 복사하는 데 오버헤드가 적기 때문이다.
 

string_view extracExtension(string_view fileName)
{
	return fileName.substr(fileName.rfind('.'));
}

 
 
함수를 이렇게 정의하면 모든 종류의 스트링에 적용할 수 있다.
 

string_view extracExtension(string_view fileName)
{
	return fileName.substr(fileName.rfind('.'));
}

string fileName = R"(c:\temp\my file.ext)";
cout << "C++ string: " << extracExtension(fileName) << endl;

const char* cString = R"(c:\temp\my file.ext)";
cout << "C string: " << extracExtension(cString) << endl;

cout << "Literal: " << extracExtension(R"(c:\temp\my file.ext)") << endl;

 
여기서 extractExtension()을 호출하는 부분에서 복제 연산이 하나도 발생하지 않는다.
extractExtension() 함수의 매개변수와 리턴 타입은 단지 포인터와 길이만 나타낸다.
그래서 굉장히 효율적이다.
 
string_view 생성자 중에서 원시(raw)버퍼와 길이를 매개변수로 받는 것도 있다.
이러한 생성자는 NUL로 끝나지 않는 스티링 버퍼로 string_view를 생성할 때 사용한다.
또한 NUL로 끝나느 스트링 버퍼를 사용할 때도 유용하다.
하지만 스트링의 길이를 이미 알고 있기 때문에 생성자에서 문자 수를 따로 셀 필요는 없다.
 
string_view를 사용하는 것만으로 string이 생성되지는 않는다.
string 생성자를 직접 호출하거나 string_view::data()로 생성해야 한다.
 
즉, 읽기 전용 스트링을 받는 함수나 메서드의 매개변수 타입은
const std::string&나 const char* 대신 std::string_view로 지정한다.
 

std::string_view 리터럴

 
표준 사용자 정의 리터럴인 'sv'를 사용하면 스트링 리터럴을 std::string_view로 만들 수 있다. 

auto sv = "My string_view"sv;

 
 
비표준 스트링
 
C++ 프로그래머 상당수가 C++ 스타일의 스트링을 잘 사용하지 않는 데는
여러가지 이유가 있다.
C++ 규격에 명확히 나오지 않기 때문에 string이라는 타입이 있는 줄도 모르는 이가 있고,
또 수년 동안 C++string을 사용하다가 별로 만족스럽지 않아서
원하는 형태로 스트링 타입을 직접 정의해서 쓰는 프로그래머도 있다.
가장 큰 이유는 마이크로소프트 MFC의 CString클래스처럼
개발 프레임워크나 운영체제에서 나름대로 정의한 스트링을 제공하기 때문이다.
주로 하위 호환성이나 레거시 문제를 해결하기 위해 이렇게 제공한다.
C++로 프로젝트를 시작할 때 구성원이 사용할 스트링을 미리 결정하는 것은 굉장히 중요하다.
그중에서도 다음 사항은 반드시 명심해야 한다.
 

  • C 스타일 스트링은 사용하지 않는다.
  • MFC나 QT등에서 기본저긍로 제공하는 스트링처럼
    현재 사용하는 프레음워크에서 제공하는 스트링을
    프로젝트의 표준 스트링으로 삼는다.
  • std::string으로 스트링을 표현한다면 함수의 매개변수로 전달할 읽기 전용 스트링은
    std::string_view로 지정한다.
    스트링을 다른 방식으로 표현한다면 현재 프레임워크에서 제공하는
    string_view와 유사한 기능을 활용한다.