[Advanced C++] 12. std::string ,std::string_view

dev.kelvin·2025년 1월 15일
1

Advanced C++

목록 보기
12/74
post-thumbnail

1. std::string

std::string

C-Style의 문자열 타입은 사용하기 어렵다 (C-Style 문자열 변수에는 새 문자열 값을 할당할 수 없다, 특정 C-Style의 문자열을 더 짧은 크기의 C-Style 문자열에 할당하면 의도치 않은 동작이 발생한다)

따라서 C++에서는 문자열 타입을 사용할 때 C-Style 문자열은 사용하지 않는것이 좋다

C++에서 문자열 타입은 std::string, std::string_view 를 사용한다, 이 타입들은 기본 타입이 아니다 (클래스 타입임)

	#include <string>
    
    std::string nickname {};

다른 타입과 마찬가지로 타입 위치에 std::string을 쓰면 문자열 타입이 된다, { }로 초기화 했기 때문에 빈 문자열로 초기화를 한 것이다

	std::string nickname { "kelvin" };
    nickname = "jacob"; //C-Style과 다르게 할당이 가능함
    
    std::string age { "10" }; //숫자도 가능, 단 문자열로 인식하기 때문에 산술연산과 같은 조작은 불가능함 (물론 변환하는 방법은 존재함)

std::cout을 통해 std::string 타입 변수를 콘솔에 출력할 수 있다

	std::cout << nickname << std::endl;

std::string의 강점 중 하나는 바로 길이가 다른 문자열을 처리할 수 있다는 것이다

	std::string name { "kelvin" };
    
    name = "kei";
    name = "Alex";

이때 문자열을 std::string 타입 변수에 할당하려고 할 때 공간이 부족하다면 동적할당을 통해 런타임에 추가 메모리 할당을 요청한다 따라서 std::string은 유연하지만 비교적 느리다

그렇다면 입력을 받을때 std::string을 사용할 수 있을까?

	std::string name{};
    std::cin >> name;
    
    std::string address{};
    std::cin >> address;
    
    std::cout << name << address << std::endl;

이때 name을 kelvin park이라고 입력하면 address의 입력은 받지 않고 name에 kelvin, address에 park이 들어가 출력되게 된다, 이는 이전에 정리한 입력 buffer문제이다

문자를 입력받고 공백을 만나면 해당 문자까지만 반환하여 입력 buffer에 있던 데이터가 name으로 들어가고 공백 뒤의 park은 입력 buffer에 남아있기 때문에 자연스럽게 다음 입력으로 들어가게 된 현상이다

따라서 문자열을 입력받을때는 std::cin보다 std::getline()을 이용하는것이 좋다

	std::getline(std::cin >> std::ws, 문자열 변수);
    
    std::string name{};
    std::getline(std::cin >> std::ws, name);

    std::string address{};
    std::getline(std::cin >> std::ws, address);

    std::cout << name << address << std::endl;

std::cin.get()은 문자 하나만 입력받는다 (공백을 포함)
std::getline()는 문자열을 받는다 (공백을 포함한다)

이제 공백을 만나도 해당 문자까지만 반환하지 않고 전체 문자열을 반환하게 된다

그렇다면 std::ws란 무엇일까?

std::ws란 입력 buffer에 남아있는 공백을 무시하게 해준다

따라서 공백이 있어도 해당 문자열까지 잘라서 반환하지 않고 전체 문자열을 반환할 수 있게 된 것이다
(위에서는 Kelvin Park을 치고 엔터를 누르게 되면 \n 공백이 남아있는데 이를 제거하여 두번째 address입력을 정상적으로 받는것이다)

std::string의 여러 기능

문자열의 길이를 알고 싶다면

	std::string name { "kelvin" };
    name.length(); //문자열의 길이를 return한다, 이때 null문자는 포함되지 않는다 (6이 반환됨)

이때 std::string::length()는 unsigned int인 size_t를 반환한다, 따라서 기본 타입에 값을 할당하고 싶다면 캐스팅이 필요하다 (암시적으로 해줌)

	int length = { static_cast<int>(name.length()) }; 

이때 signed int인 std::ptrdiff_t로 반환하려면 다음과 같이 가능하다 (C++20)

	int length = { static_cast<int>(std::ssize(name)) }; //마찬가지로 캐스팅 필요

std::string을 함수의 인자로 넘길때 값을 전달하는건 좋지않다, 함수의 매개변수는 함수 호출 시 인스턴스화 되고 인자로 복사 초기화 되기 때문이다 (복사 비용이 크게 발생함)

대신 std::string_view를 사용한다

그렇다면 std::string타입을 반환하게 되면 어떨까?

객체를 값 타입으로 return하기 때문에 caller가 사용할 새 객체를 그대로 복사해서 만들기 때문에 복사 비용이 발생할 것 같지만 그렇지않다 (move semantics 덕분)

일단 std::string을 값으로 반환해도 괜찮은 경우는 다음과 같다

  • local변수인 std::string 타입의 값 반환 (함수 내부에서 생성된 std::string은 return과 동시에 move처리 된다 (복사X)
  • 다른 함수가 반환한 std::string 반환 (move되기 때문에 복사X)
  • 임시객체 std::string 반환 (move)
	using namespace std::string_literals;
	std::string foo()
    {
    	return "Temp"s;
    }

대부분의 std::string 외 객체들을 return할때는 값타입으로 반환하지 않는게 좋다 (복사 오버헤드가 크게 발생함, 단 요즘 컴파일러는 복사 생략, 이동 세멘틱으로 오버헤드가 크게 발생하지 않음)

C-Style의 literal string은 "문자열"로 사용하는데 이를 쉽게 std::string 타입의 literal문자열로 변환할 수 있다

	using namespace std::string_literals;
    
	std::string foo { "kelvin"s };

s suffix를 붙여 C-Style 문자열을 std::string 임시객체로 만든다 (C++11)

다른 방식으로는

	std::string { "Kelvin", 6 }; //null문자 제외한 length

이렇게도 C-Style 문자열을 std::string을 만들 수 있다 (생성자 호출)

마지막으로 std::string은 constexpr로 정의하게 되면 컴파일 에러가 발생할 수 있다(std::string은 런타임 동적할당 클래스 객체이기 때문)
따라서 필요한 경우 std::string_view를 사용하면 된다


2. std::string_view

std::string_view

C++17부터는 std::string의 복사 비용이 많이 드는 문제를 해결하기 위해 std::string_view를 도입했다

이는 복사하지 않고 기존 문자열에 대한 읽기 전용 엑세스를 제공한다 (원본 문자열에 대한 참조)

	void testString(std::string_view InString)
    {
        std::cout << InString << '\n';
    }

    int main() 
    {
        std::string name{ "kelvin" };

        testString(name);

        return 0;
    }

이렇게 복사과정 없이 std::string을 함수의 인자로 넘길 수 있다

std::string_view는 C-Style의 문자열, std::string, std::string_view 전부 초기화가 가능하다
(매개변수의 인자 전달에도 전부 가능하다 -> testString()의 인자로 3개의 타입 전부 다 가능)

std::string_view는 std::string으로의 암시적 변환이 일어나지 않는다, 이유는 std::string은 초기화 할 때 복사가 일어나기 때문이다, 따라서 string_view -> string으로 암시적 변환이 일어나면 복사도 일어날 수 있기 때문이다
(애초에 string_view는 문자열을 복사하지 않고 기존 문자열에 대한 참조로 읽기 전용 엑세스만 지원하기 때문에 복사가 일어나면 안됨)

	std::string_view viewtest{ "Hello" };
    std::string test = viewtest //error (암시적 형변환 방지)

하지만 강제로 하는 방법은 있다 (형변환, 초기화 할 때 string_view변수로 넣기)

	std::string_view viewtest { "Hello" };
	std::string test{ viewtest }; //std::string 생성자를 이용한 string_view 데이터로의 초기화 (std::string의 초기화로 인해 복사비용 발생)
    
    static_cast<std::string>(viewtest); //강제 형변환

std::string_literals의 s suffix와 마찬가지로 std::string_view_literals의 sv suffix로 C-Style의 문자열을 string_view로 만들 수 있다

	using namespace std::string_view_literals;
    
	std::cout << "Hello"sv;

또한 string_view는 읽기 전용이기 때문에 constexpr이 지원된다

	constexpr std::string_view test { "Hello" };
    std::cout << test << '\n'; //컴파일 타임에 test가 "Hello"로 치환된다

std::string가 초기화 시 높은 코스트의 사본을 만드는 이유

우선 객체가 인스턴스화 되면 해당 객체에 메모리가 할당되어 수명동안 사용해야 하는 모든 데이터가 저장된다

이렇게 할당된 메모리는 객체를 위해 할당되고 객체가 존재하는 한 메모리도 존재하도록 보장되는 안전한 공간이다

std::string 및 대부분의 객체들은 생성하고 추후에 엑세스하여 조작할 수 있고 초기값과는 독립적인 값을 가질 수 있도록 초기값을 해당 객체에 할당된 메모리에 복사한다
(복사되었기 때문에 객체는 초기값에 더이상 의존하지 않는다)

이렇게 초기값에 독립적인건 좋은 방식이다

  • 만약 초기화 값이 임시객체, 임시값이면 값은 사용 후 파괴되기 때문에 접근하지 않는게 좋다
  • 의도치않은 원본값 수정이 일어나지 않는것이 좋다

따라서 이러한 장점으로 더 코스트가 높은 복사 초기화가 이루어지는것이다

하지만 무조건적으로 복사가 필요하진 않다, 분명히 코스트가 높은 복사가 필요없는 경우는 존재한다

	#include <string>
    
    void printString(std::string s)
    {
        std::cout << s << std::endl;
    }

    int main() 
    {
        std::string teststring{ "Kelvin" };

        printString(teststring);

        return 0;
    }

printString()이 호출되며 teststring 값이 복사되어 s에 전달되고 콘솔 출력 뒤 파괴될 것이다

여기서는 teststring이 이미 출력할 string을 들고 있기 때문에 복사를 하지 않아도 된다

이때 복사를 하지 않는다면 다음과 같은 조건을 잘 체크해야 한다

  • 원본 문자열이 함수 내부에서 파괴되는가?
  • 원본 문자열이 수정되는가?

참조나 포인터를 사용할 수도 있지만 std::string_view를 통해서도 사본을 만들지 않고 넘길 수 있다

std::string_view는 말 그대로 읽기전용 뷰어이다, 따라서 원본을 수정할 수는 없지만 참조는 가능하다

이때 중요한 점은 string_view로 참조하고 있는 string이 변경되거나 파괴되면 의도치 않은 동작이 발생한다는 것이다 (특정 포인터나 참조가 파괴된 객체를 참조하는것을 dangling view라고 한다 (유효하지 않은 메모리 참조))

    std::string_view vw{};

    {
        std::string teststring{ "Kelvin" };
        vw = teststring;
    }

    std::cout << vw << std::endl; //{ }에서 이미 teststring은 local var로서 파괴되기 때문에 string_view인 vw는 파괴된 값을 참조한다 (dangling view)
    std::string getName()
    {
        std::string s { "Kelvin" };
        return s;
    }

    int main()
    {
      std::string_view name { getName() }; //getName()으로 나온 return값은 임시객체임, 따라서 즉시 파괴되기 때문에 밑에서 string_view는 파괴된 값을 참조하게 된다
      std::cout << name << '\n';

      return 0;
    }
	int main()
    {
        using namespace std::string_literals;
        std::string_view name { "Kelvin"s };
        std::cout << name << '\n'; //"Kelvin"s는 임시객체 string이기 때문에 바로 파괴되어 string_view이 name은 파괴된 객체를 참조하게 된다

        return 0;
    }
	int main() 
    {
      	std::string str = "Hello, World!";
      	std::string_view view = str; // str의 데이터를 참조

     	 str = "This is a much longer string, which causes reallocation!"; 
     	 // str의 데이터가 바뀌면서 메모리가 재할당됨.
      	 // 기존 데이터는 더 이상 유효하지 않음.

      	std::cout << "View: " << view << std::endl; // Dangling view (유효하지 않은 메모리 참조)
      return 0;
  }

메모리가 재할당되면 이전 문자열 데이터에서 사용하던 메모리를 OS로 반환한다, 이때 string_view는 이전 메모리를 참조하기 때문에 dangling view가 일어나는 것이다

더 긴 문자열로 수정되면 수정된 문자열이 기존 문자열 길이보다 초과 되는 부분은 쓰레기 값으로 나오고, 더 짧은 문자열로 수정되면 기존 문자열의 길이까지 수정한 문자열로 나오게된다

이때 여기서 view = str로 수정한 뒤 다시 할당해주면 정상적으로 참조하게 된다

    int main() 
    {
        std::string str = "Hello, World!";
        std::string_view view = str; // str의 데이터를 참조

        str.replace(0, 5, "Hi"); // 기존 메모리 주소에 새로운 데이터를 덮어씀.

        std::cout << "String: " << str << std::endl; // 수정된 str 출력
        std::cout << "View: " << view << std::endl; // View는 여전히 "Hello, World!"의 길이를 유지
        return 0;
    }

기존의 str과 같은 길이의 데이터를 저장하기 때문에 메모리 재할당이 이루어지지 않는다 따라서 view는 수정되기 전의 string을 참조한다

따라서 string_view는 string_literals로 초기화 하는 방식은 좋지않다, string_view_literals나 C-Style 문자열로 초기화 하는게 좋다 (string_literals로 초기화 하면 임시 객체가 생성되어 dangling pointer참조 가능성이 높다, C-style 문자열 같은 경우에는 static memory 영역을 참조하기 때문에 문제 없음)

std::string_view의 좋은 용도

std::string_view는 위에서 설명했듯 읽기 전용 함수 매개변수로 사용하는것이 제일 좋은 용도이다
(사본을 만들지 않고 문자열 전달이 가능)

	#include <string_view>

    void printString(std::string_view s)
    {
        std::cout << s << std::endl;
    }

    int main() 
    {
        std::string teststring{ "Kelvin" };

        printString(teststring);

        return 0;
    }

매개변수가 std::string_view이기 때문에 teststring이 복사되지 않고 참조 방식으로 전달된다

일반적으로 std::string&보다 std::string_view가 더 선호된다

std::string_view는 함수 return값으로 사용할 수 있지만 위험하다

	std::string_view printString()
    {
        std::string temp{ "temp" };

        return temp;
    }

로컬변수는 함수 종료시 파괴되기 때문에 파괴된 변수를 참조하는 string_view가 return된다, 이는 dangling view를 발생시킨다

std::string_view를 함수의 return값으로 사용할 때 안전한 방법은 C-Style 문자열을 return하는 것이다

	std::string_view printString()
    {

        return "temp";
    }

또한 string_view 타입 매개변수를 return하는것도 안전하다

	std::string_view printString(std::string_view vw1)
    {

        return vw1;
    }
    
    int main()
    {
    	std::string s{ "Hello" };
        
        printString(s);
    }

참조하는 string인 s가 유효하기 때문에 이를 참조하는 string_view 매개변수를 return하는건 안전하다

이때 임시객체를 인자로 넘긴다면 해당 string_view를 넘기면 마찬가지로 dangling view가 발생한다
(ex) 함수 return값으로 string을 인자로 넘기는 방식)

remove_prefix(n), remove_suffix(n)

실제 문자열 원본값을 수정하지는 않지만 string_view에서는 수정해서 볼 수 있다

	int main()
    {
        std::string_view str = "Hello";

        str.remove_prefix(1); //ello

        std::cout << str << std::endl;

        str.remove_suffix(2); //el

        std::cout << str << std::endl;
		
        str = "Hello";
        
        return 0;
    }

prefix는 왼쪽부터 문자 제거, suffix는 오른쪽부터 문자 제거이다

다시 원본 문자열로 string_view를 재할당해야 원본 문자열이 출력된다

이때 string_view는 항상 null문자로 끝난다고 가정하고 코드를 작성하면 안된다

위에서 str.remove_suffix(2)가 실행된 시점에서 el이 출력되게되는데, el뒤는 l이기 때문에 null문자가 아니다

std::string, std::string_view 언제 무엇을 선택해서 쓰는게 좋을까?

<변수>
std::string

  • 수정할 수 있는 문자열일때
  • 사용자 입력 문자열을 받을때
  • std::string을 반환하는 함수의 값을 받을때

std::string_view

  • valid한 문자열에 대해 원본 수정이 필요 없고 단순 읽기 전용 엑세스만 필요할 때
  • constexpr C-Style의 문자열을 받을때
  • 함수가 반환한 문자열을 읽기 전용으로 계속 참조해야 할 때 (dangline view 조심)

매개변수는 원본 문자열을 수정할때, 수정하지 않을때에 나누어 string&, string_view를 선택해서 사용하면 된다

만약 C++17 이전 버전이라면 string_view가 지원되지 않기 때문에 const std::string&로 사용하여 복사 오버헤드를 줄이는 방식 + 원본 수정X 하는 방식으로 사용하자

<return값>

std::string

  • local var이거나 매개변수를 return할때
  • 내부에서 함수 혹은 연산자로 std::string을 반환하는경우

std::string_view

  • C-Style문자열, std::string_view_literals로 초기화 된 local var반환
  • std::string_view 매개변수를 반환할 때

string, string view의 효율적 사용

  • std::string은 복사 비용이 크기 때문에 참조를 사용하거나 string_view를 이용하여 오버헤드를 줄이자
  • 짧게 사용될 string객체 생성을 피하자 ex) 한번 출력하고 버려질 std::string("foo"); 를 생성하여 객체를 생성하는 방식
  • std::string을 return하면 move되어 큰 비용이 발생하지 않는다
  • std::string_view는 literal C-Style 문자열에 적합하다 (안전하게 참조가 가능)
  • std::string이 파괴되면 참조하는 string_view는 valid하지 않다
  • std::string_view의 끝은 null문자가 아닐 수 있다
profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글