O arquivo `string.assign (string.data (), 5)` está bem definido ou é UB?

11

Um colega de trabalho queria escrever isso:

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

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

Eu disse que o retorno string_viewme deixava desconfortável a priori e, além disso, o apelido aqui parecia UB para mim.

Posso dizer com certeza que line = strip_whitespace(line), neste caso, é equivalente a line = std::string_view(line.data(), 5). Eu acredito que irá chamar string::operator=(const T&) [with T=string_view], que é definido para ser equivalente a line.assign(const T&) [with T=string_view], que é definido para ser equivalente a line.assign(line.data(), 5), que é definido para fazer isso:

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.

Mas isso não diz o que acontece quando existe um alias.

Ontem fiz esta pergunta no Slack cpplang e recebi respostas contraditórias. Procurando respostas super autoritativas aqui e / ou análise empírica das implementações de fornecedores de bibliotecas reais.


Eu escrevi casos de teste para string::assign, vector::assign, deque::assign, list::assign, e forward_list::assign.

  • O Libc ++ faz com que todos esses casos de teste funcionem.
  • O Libstdc ++ faz com que todos funcionem, com exceção de forward_list, que segfaults.
  • Não conheço a biblioteca da MSVC.

O segfault no libstdc ++ me dá esperança de que este seja UB; mas também vejo o libc ++ e o libstdc ++ fazendo um grande esforço para fazer esse trabalho pelo menos nos casos mais comuns.

Quuxplusone
fonte
Você compilou os casos de teste com o ASan e / ou os executou no Valgrind? Isso eliminaria a possibilidade de o código causar violações de acesso, embora ainda possa funcionar na prática, e não por definição.
Konrad Rudolph
11
"Se qualquer função membro ou operador de basic_string lança uma exceção, essa função ou operador não tem outro efeito no objeto basic_string." - isso força a alocação de armazenamento a ocorrer antes do armazenamento existente ser liberado, de modo que uma exceção seja lançada se a alocação falhar, sem alterar *this. Mas não vejo nada para impedir a reutilização do armazenamento existente; nesse caso, isso se torna não especificado, uma vez que a semântica da substituição do armazenamento não é especificada.
Sam Varshavchik
2
Para os contêineres de sequência mencionados, certamente é o UB, devido à violação das condições prévias nos assignrequisitos em [tab: container.seq.req] .
noz

Respostas:

8

Exceto algumas exceções das quais a sua não é uma, chamar uma função de membro que não seja const (ou seja assign) em uma string invalida [...] ponteiros para seus elementos. Isso viola a pré-condição de assignque [s, s + n)é um intervalo válido, então este é um comportamento indefinido.

Observe que o string::operator=(string const&)idioma é específico para tornar a auto-atribuição uma opção não operacional.

ecatmur
fonte
11
Então, qual é exatamente o ponto de invalidação e o ponto em que a condição prévia é requerida? A resposta parece estar assumindo que a pré-condição deve se manter após a chamada da função de membro.
noz
11
@walnut Eu não sou advogado de idiomas (nem uma pessoa com um conhecimento C ++ particularmente extenso), mas quando invertemos seu cenário, podemos fazer uma pergunta - o intervalo pode ser invalidado durante a execução de assign? Se sim, teríamos que definir um ponto específico dentro da implementação do assign para marcar quando exatamente a invalidação pode ocorrer, e acredito que isso não é algo que o C ++ faria. Eu poderia estar errado.
Fureeish
2
@Fureeish Também não conheço, mas veja, por exemplo, a edição 526 do LWG , fechada como " não é um defeito ", que menciona em sua recomendação de fechamento que std::vector::insert(iterator pos, const T& value)deve funcionar se valueestiver no próprio vetor, porque o padrão não especifica que ele tem permissão para não funcionar, mesmo que essa referência possa ser invalidada pela chamada.
noz
11
@walnut " é necessário para o trabalho porque o padrão não dar permissão para que ele não funcione. " - amor-lo . Sooo ... vale a pena perguntar o que acontece na prática ? A implementação é necessária para fazer uma cópia do argumento nessa situação? Como você poderia implementá-lo realisticamente ..? Já ouvi falar em padrões que exigem que os compiladores façam o impossível - é um desses casos? Independentemente disso, obrigado pelo comentário!
Fureeish
11
@Fureeish Na verdade, meu exemplo anterior (agora excluído) não estava realmente testando o que eu queria testar. Aqui está um exemplo fixo, mostrando que o libc ++ e o libstdc ++ realmente copiam antes de avançar na realocação, conforme necessário.
noz