Czy `string.assign (string.data (), 5)` jest dobrze zdefiniowany czy UB?

11

Współpracownik chciał to napisać:

std::string_view strip_whitespace(std::string_view sv);

std::string line = "hello  ";
line = strip_whitespace(line);

Powiedziałem, że powrót string_viewsprawił, że poczułem niepokój a priori , a ponadto aliasing tutaj wyglądał dla mnie jak UB.

Mogę z całą pewnością stwierdzić, że line = strip_whitespace(line)w tym przypadku jest to odpowiednik line = std::string_view(line.data(), 5). Wierzę, że zadzwoni string::operator=(const T&) [with T=string_view], która jest zdefiniowana jako równoważna line.assign(const T&) [with T=string_view], która jest zdefiniowana jako równoważna line.assign(line.data(), 5), która jest zdefiniowana w tym celu:

Preconditions: [s, s + n) is a valid range.
Effects: Replaces the string controlled by *this with a copy of the range [s, s + n).
Returns: *this.

Ale to nie mówi, co się stanie, gdy pojawi się aliasing.

Zadałem to pytanie wczoraj na cpplang Slack i otrzymałem mieszane odpowiedzi. Poszukuję tutaj autorytatywnych odpowiedzi i / lub analizy empirycznej implementacji prawdziwych dostawców bibliotek.


Pisałem przypadków testowych na string::assign, vector::assign, deque::assign, list::assign, i forward_list::assign.

  • Libc ++ sprawia, że ​​wszystkie te przypadki testowe działają.
  • Libstdc ++ sprawia, że ​​wszystkie działają, z wyjątkiem tego forward_list, co segfault.
  • Nie wiem o bibliotece MSVC.

Segfault w libstdc ++ daje mi nadzieję, że jest to UB; ale widzę też, że zarówno libc ++, jak i libstdc ++ wkładają wiele wysiłku, aby to zadziałało przynajmniej w typowych przypadkach.

Quuxplusone
źródło
Czy skompilowałeś przypadki testowe za pomocą ASan i / lub uruchomiłeś je pod Valgrind? To wyeliminowałoby zgadywanie, czy kod powoduje naruszenie praw dostępu, chociaż może nadal działać w praktyce, a nie z definicji.
Konrad Rudolph
1
„Jeśli jakakolwiek funkcja członkowska lub operator Basic_string zgłasza wyjątek, ta funkcja lub operator nie ma innego wpływu na obiekt basic_string.” - wymusza to alokację pamięci, zanim istniejąca pamięć zostanie zwolniona, tak że wyjątek zostanie zgłoszony w przypadku niepowodzenia alokacji, bez zmiany *this. Ale nie widzę nic, co mogłoby uniemożliwić ponowne wykorzystanie istniejącej pamięci, w którym to przypadku nie jest to określone, ponieważ semantyka kopiowania pamięci nie jest określona.
Sam Varshavchik
2
W przypadku wspomnianych kontenerów sekwencji jest to z pewnością UB, z powodu naruszenia warunków wstępnych assignw [tab: container.seq.req] .
orzech

Odpowiedzi:

8

Pomijając kilka wyjątków, z których twój nie jest jeden, wywołanie funkcji non-const członka (tj. assign) W łańcuchu unieważnia [...] wskaźniki do jego elementów. To narusza warunek na assignktóry [s, s + n)jest ważny zakres, więc jest to niezdefiniowane zachowanie.

Zauważ, że string::operator=(string const&)ma język specjalnie, aby samodzielne przypisywanie stało się niemożliwe.

ecatmur
źródło
1
Czym więc dokładnie jest punkt unieważnienia i moment, w którym należy spełnić warunek wstępny? Odpowiedź wydaje się zakładać, że warunek wstępny musi zostać spełniony po wywołaniu funkcji składowej.
orzech
1
@walnut Nie jestem prawnikiem językowym (ani osobą o szczególnie rozszerzonej znajomości C ++), ale kiedy odwrócimy Twój scenariusz, możemy zadać pytanie - czy zakres może zostać unieważniony podczas wykonywania assign? Jeśli tak, to musielibyśmy ustawić konkretny punkt w implementacji przypisania, aby zaznaczyć, kiedy dokładnie może nastąpić unieważnienie, i uważam, że to nie byłoby coś, co zrobiłby C ++. Mogę się jednak mylić.
Fureeish
2
@ Fureeish Też nie wiem, ale patrz np. Problem LWG 526 , zamknięty jako „ nie defekt ”, który wspomina w swoim zaleceniu dotyczącym zamknięcia, które std::vector::insert(iterator pos, const T& value)musi działać, jeśli valuejest w samym wektorze, ponieważ standard nie określa tego może nie działać, nawet jeśli odwołanie może zostać unieważnione przez połączenie.
orzech
1
@walnut „ jest wymagany do działania, ponieważ standard nie zezwala na to, aby nie działał. ” - uwielbiam to . Sooo ... czy warto zapytać, co dzieje się w praktyce ? Czy wdrożenie wymaga wykonania kopii argumentu w takiej sytuacji? Jak możesz to realistycznie wdrożyć ...? Słyszałem o standardach wymagających kompilatorów do robienia rzeczy niemożliwych - czy to jeden z tych przypadków? Niezależnie od tego dziękuję za komentarz!
Fureeish
1
@Fureeish Właściwie mój poprzedni (teraz usunięty) przykład nie testował tego, co chciałem przetestować. Oto ustalony przykład pokazujący, że zarówno libc ++, jak i libstdc ++ faktycznie kopiują przed przejściem do realokacji zgodnie z wymaganiami.
orzech