Zadanie

Dany jest łańcuch tekstowy:

#include <iostream>
#include <string>

int main()
{
   std::string hello = "\"Hello World !!!\"";

   // Implement your solution here


   std::cout << hello << '\n';
}

Uruchom w edytorze

"Hello World !!!"

Zaimplementuj rozwiązanie usuwające cudzysłowy.

Rozwiązanie

Zadanie to jest proste bowiem sprowadza się do usunięcia pierwszego i ostatniego znaku w łańcuchu tekstowym przechowywanym w obiekcie typu std::string (plik nagłówkowy string). Oczywiście posługiwać będziemy się interfejsem oferowanym przez klasę std::string. Warto jednak zauważyć, że nawet tak prosty problem możemy rozwiązać na przynajmniej kilka sposobów. Oczywiście operacja usuwania pierwszego i ostatniego znaku ma sens wyłącznie dla łańcuchów tekstowych składających się przynajmniej z dwóch znaków (w naszym przypadku mogłoby to oznaczać łańcuch tekstowy składający się wyłącznie z dwóch cudzysłowów). Dlatego też każde z przedstawionych tu rozwiązań będzie objęte instrukcją if:

if (hello.size() >= 2)
{
   // ...
}

W przeciwnym razie możemy doprowadzić do niezdefiniowanego zachowania (ang. undefined behaviour) lub rzucenia wyjątku (ang. exception) w zależności od użytej metody (wyjątek może być jawnie wyspecyfikowany np. std::out_of_range  lub sam w sobie być efektem niezdefiniowanego zachowania).

Metoda std::string::erase + std::string::pop_back

#include <iostream>
#include <string>

int main()
{
   std::string hello = "\"Hello World !!!\"";

   // Implement your solution here
   if (hello.size() >= 2)
   {
      hello.erase(std::begin(hello));
      hello.pop_back();
   }

   std::cout << hello << '\n';
}

Uruchom w edytorze

Hello World !!!

W pierwszym rozwiązaniu wykorzystujemy metodę std::string::pop_back (dostępnej od standardu C++11), która usuwa nam ostatni znak z łańcucha tekstowego. Niestety podobnie jak w przypadku kontenera std::vector (plik nagłówkowy vector) klasa std::string nie posiada metody pop_front, ponieważ wiązałoby się to z koniecznością przeniesienia każdego znaku na poprzednią pozycję, w efekcie czego metoda taka byłaby nieefektywna z definicji (złożoność rosłaby liniowo wraz ze wzrostem długości łańcucha tekstowego). Dla porównania metoda std::string::pop_back usuwa ostatni znak w stałym czasie (ponownie jest to analogiczne zachowanie jak w przypadku kontenera std::vector). Aby usunąć pierwszy znak posłużymy się bardziej ogólną metodą usuwającą std::string::erase. W powyższym rozwiązaniu skorzystałem z wersji tej metody przyjmującej iterator wskazujący na usuwany znak. Ponieważ interesuje nas pierwszy znak skorzystamy z iteratora początkowego, który możemy otrzymać za pomocą metody std::string::begin lub generycznej funkcji std::begin (plik nagłówkowy iterator).

Druga wersja metody std::string::erase wykorzystuje indeks pierwszego znaku do usunięcia oraz liczbę znaków do usunięcia. W przypadku usunięcia pierwszego znaku indeks będzie miał wartość ”0”, natomiast liczba znaków to oczywiście ”1”. Oto zmodyfikowane rozwiązanie:

#include <iostream>
#include <string>

int main()
{
   std::string hello = "\"Hello World !!!\"";

   // Implement your solution here
   if (hello.size() >= 2)
   {
      hello.erase(0, 1);
      hello.pop_back();
   }

   std::cout << hello << '\n';
}

Uruchom w edytorze

Hello World !!!

Uważam, że rozwiązanie oparte na wersji metody std::string::erase przyjmującej iterator jest bardziej czytelne. Niemniej jednak nie jestem zwolennikiem powyższego rozwiązania jako całości, ze względu na pewną ”asymetrię”. Otóż do usuwania znaków używamy tutaj dwóch różnych metod, a jedyna różnica polega na położeniu tych znaków. Dlatego też kolejne dwa rozwiązania będą oparte wyłącznie na ogólnej metodzie std::string::erase.

Metoda std::string::erase (wersja z indeksem)

#include <iostream>
#include <string>

int main()
{
   std::string hello = "\"Hello World !!!\"";

   // Implement your solution here
   if (hello.size() >= 2)
   {
      hello.erase(0, 1);
      hello.erase(hello.size() - 1);
   }

   std::cout << hello << '\n';
}

Uruchom w edytorze

Hello World !!!

Korzystamy tutaj ze wspomnianej wcześniej wersji metody std::string::erase opartej na indeksie do pierwszego usuwanego znaku oraz liczbie znaków do usunięcia. W przypadku pierwszego znaku podajemy oba parametry (tak samo jak w pierwszym rozwiązaniu):

hello.erase(0, 1);

Po wywołaniu tej metody rozmiar łańcucha tekstowego zostanie oczywiście zaktualizowany. Rozmiar ten wykorzystujemy do usunięcia ostatniego znaku, bowiem indeks ostatniego znaku równy jest co do wartości liczbie znaków w łańcuchu tekstowym pomniejszonej o ”1”. W tym celu posługujemy się metodą std::string::size (równoważnie można skorzystać z metody std::string::length):

hello.erase(hello.size() - 1);

W przypadku ostatniego elementu nie musimy podawać drugiego parametru mówiącego o liczbie znaków do usunięcia, bowiem domyślnie metoda  std::string::erase usuwa wszystkie znaki do końca łańcucha tekstowego (odpowiada za to domyślna wartość tego parametru w postaci std::string::npos).

Metoda std::string::erase (wersja z iteratorem)

Stosując metodę std::string::erase udało nam się wprowadzić ”symetrię” do naszego rozwiązania. Poprzednie rozwiązanie nie należy jednak do najbardziej czytelnych (pojawiają się ”magiczne liczby”) i najłatwiejszych (musimy podawać dwa argumenty o określonym znaczeniu). W tym punkcie przedstawię rozwiązanie preferowane przeze mnie, a które oparte jest na wersji metody std::string::erase przyjmującej pojedynczy iterator:

#include <iostream>
#include <string>

int main()
{
   std::string hello = "\"Hello World !!!\"";

   // Implement your solution here
   if (hello.size() >= 2)
   {
      hello.erase(std::begin(hello));
      hello.erase(std::prev(std::end(hello)));
   }

   std::cout << hello << '\n';
}

Uruchom w edytorze

Hello World !!!

Pierwszy znak usuwany wykorzystując iterator początkowy zwracany przez metodę std::string::begin lub generyczną funkcję std::begin (tak samo jak w pierwszym rozwiązaniu):

hello.erase(std::begin(hello));

W przypadku usuwania ostatniego elementu musimy jedynie pamiętać, iż iterator końcowy zwracany przez metodę std::string::end lub generyczną funkcję std::end nie wskazuje na ostatni znak w łańcuchu tekstowym, ale tuż za nim (iterator końcowy spełnia tutaj analogiczną funkcję jak znak terminujący ‚\0‚ w przypadku literału tekstowego w stylu języka C). Oznacza to, że potrzebujemy pobrać iterator poprzedzający iterator końcowy. W tym celu możemy posłużyć się generyczną funkcją std::prev (plik nagłówkowy iterator):

hello.erase(std::prev(std::end(hello)));

Rozwiązanie to jest przede wszystkim bardziej czytelne (funkcje std::begin oraz std::end lepiej niż ”magiczne liczby” wyrażają naszą intencję), a także prostsze (tylko jeden parametr), zachowując przy tym swoją ”symetrię”.