Passando ponteiros compartilhados como argumentos

88

Se eu declarar um objeto envolvido em um ponteiro compartilhado:

std::shared_ptr<myClass> myClassObject(new myClass());

então eu queria passá-lo como um argumento para um método:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

O acima simplesmente incrementa a contagem de referência do shared_pt e está tudo bem? Ou deixa um ponteiro pendurado?

Você ainda deveria fazer isso ?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Acho que a 2ª forma pode ser mais eficiente porque só precisa copiar 1 endereço (em oposição ao ponteiro inteligente inteiro), mas a 1ª forma parece mais legível e não prevejo ultrapassar os limites de desempenho. Só quero ter certeza de que não há nada de perigoso nisso.

Obrigado.

Steve H
fonte
14
const std::shared_ptr<myClass>& arg1
Capitão Óbvio
3
A segunda maneira está quebrada, a primeira é idiomática se você realmente precisa que sua função compartilhe a propriedade. Mas, DoSomethingrealmente precisa compartilhar a propriedade? Parece que deveria ter apenas uma referência em vez disso ...
ildjarn
8
@SteveH: Não precisa, mas por que sua função deveria forçar uma semântica especial de propriedade de objeto em seus chamadores se ela realmente não precisa deles? Sua função não faz nada que não seja melhor do que void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: Todo o propósito dos smart pointers é lidar com os problemas comuns de propriedade de objetos - se você não os tem, não deveria usar smart pointers em primeiro lugar. ; -] shared_ptr<>Especificamente, você deve passar por valor para realmente compartilhar a propriedade.
ildjarn
4
Não relacionado: em geral, std::make_sharednão é apenas mais eficiente, mas também mais seguro que o std::shared_ptrconstrutor.
R. Martinho Fernandes

Respostas:

171

Quero passar um ponteiro compartilhado para uma função. Você pode me ajudar com isso?

Claro, posso ajudar com isso. Suponho que você tenha algum conhecimento da semântica de propriedade em C ++. Isso é verdade?

Sim, estou razoavelmente confortável com o assunto.

Boa.

Ok, só consigo pensar em duas razões para shared_ptrargumentar:

  1. A função deseja compartilhar a propriedade do objeto;
  2. A função executa alguma operação que funciona especificamente em shared_ptrs.

Em qual você está interessado?

Estou procurando uma resposta geral, então estou interessado em ambas. Estou curioso para saber o que você quer dizer no caso nº 2, no entanto.

Exemplos de tais funções incluem std::static_pointer_castcomparadores customizados ou predicados. Por exemplo, se você precisa encontrar todos os shared_ptr únicos de um vetor, você precisa de tal predicado.

Ah, quando a função realmente precisa manipular o próprio ponteiro inteligente.

Exatamente.

Nesse caso, acho que devemos passar por referência.

Sim. E se não mudar o ponteiro, você deseja passar pela referência const. Não há necessidade de copiar, pois você não precisa compartilhar a propriedade. Esse é o outro cenário.

OK, entendi. Vamos falar sobre o outro cenário.

Aquele onde você compartilha a propriedade? Está bem. Como você compartilha a propriedade com shared_ptr?

Ao copiá-lo.

Então a função precisará fazer uma cópia de um shared_ptr, correto?

Obviamente. Então, passo por uma referência a const e copio para uma variável local?

Não, isso é uma pessimização. Se for passado por referência, a função não terá escolha a não ser fazer a cópia manualmente. Se for passado por valor, o compilador escolherá a melhor escolha entre uma cópia e uma movimentação e executará automaticamente. Então, passe por valor.

Bom ponto. Devo me lembrar daquele artigo " Quer velocidade? Passe por valor. " Com mais frequência.

Espere, e se a função armazenar o shared_ptrem uma variável de membro, por exemplo? Isso não seria uma cópia redundante?

A função pode simplesmente mover o shared_ptrargumento para seu armazenamento. Mover a shared_ptré barato porque não altera nenhuma contagem de referência.

Ah, boa ideia.

Mas estou pensando em um terceiro cenário: e se você não quiser manipular shared_ptrnem compartilhar a propriedade?

Nesse caso, shared_ptré completamente irrelevante para a função. Se você deseja manipular a ponta, pegue uma ponta e deixe que os chamadores escolham a semântica de propriedade que desejam.

E devo tomar a ponta por referência ou por valor?

As regras usuais se aplicam. Os indicadores inteligentes não mudam nada.

Passe por valor se vou copiar, passe por referência se quiser evitar uma cópia.

Direito.

Hmm. Acho que você esqueceu mais um cenário. E se eu quiser compartilhar a propriedade, mas apenas dependendo de uma determinada condição?

Ah, um caso interessante. Não espero que isso aconteça com frequência. Mas quando isso acontecer, você pode passar por valor e ignorar a cópia, se não precisar, ou passar por referência e fazer a cópia, se precisar.

Arrisco uma cópia redundante na primeira opção e perco um movimento potencial na segunda. Não posso comer o bolo e ficar com ele também?

Se estiver em uma situação em que isso realmente importa, você pode fornecer duas sobrecargas, uma tomando uma referência de valor constante e outra tomando uma referência de valor r. Um copia, o outro se move. Um modelo de função de encaminhamento perfeito é outra opção.

Acho que isso cobre todos os cenários possíveis. Muito obrigado.

R. Martinho Fernandes
fonte
2
@Jon: Para quê? Isto é tudo sobre devo fazer uma ou devo fazer B . Acho que todos nós sabemos passar objetos por valor / referência, não?
sbi de
9
Porque prosa como "Isso significa que vou passar uma referência a const para fazer a cópia? Não, isso é uma pessimização" é confusa para um iniciante. Passar por uma referência a const não faz uma cópia.
Jon
1
@Martinho discordo da regra "Se quiser manipular a ponta, tira uma ponta". Conforme declarado nesta resposta, não assumir a propriedade temporária de um objeto pode ser perigoso. Em minha opinião, o padrão deve ser passar uma cópia para propriedade temporária para garantir a validade do objeto durante a chamada de função.
radman
@radman Nenhum cenário na resposta que você vincula aplica essa regra, então é completamente irrelevante. Por favor, escreva-me um SSCCE onde você passa por referência de um shared_ptr<T> ptr;(ou seja void f(T&), chamado com f(*ptr)) e o objeto não sobrevive à chamada. Alternativamente, escreva um em que você passe por valor void f(T)e por problemas.
R. Martinho Fernandes
@Martinho verifique meu código de exemplo para o exemplo correto simples e independente. O exemplo é para void f(T&)e envolve threads. Acho que meu problema é que o tom da sua resposta desencoraja a passagem da propriedade de shared_ptr's, que é a maneira mais segura, intuitiva e menos complicada de usá-los. Eu seria extremamente contra aconselhar um iniciante a extrair uma referência aos dados pertencentes a um shared_ptr <> as possibilidades de uso indevido são grandes e o benefício é mínimo.
radman
22

Acho que as pessoas estão desnecessariamente com medo de usar ponteiros brutos como parâmetros de função. Se a função não for armazenar o ponteiro ou afetar seu tempo de vida, um ponteiro bruto funciona da mesma forma e representa o menor denominador comum. Considere, por exemplo, como você passaria a unique_ptrem uma função que recebe a shared_ptrcomo parâmetro, por valor ou por referência const?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Um ponteiro bruto como um parâmetro de função não impede que você use ponteiros inteligentes no código de chamada, onde realmente importa.

Mark Ransom
fonte
8
Se você estiver usando um ponteiro, por que não usar apenas uma referência? DoSomething(*a_smart_ptr)
Xeo
5
@Xeo, você está certo - isso seria ainda melhor. Às vezes, você precisa permitir a possibilidade de um ponteiro NULL.
Mark Ransom
1
Se você tem uma convenção de usar ponteiros brutos como parâmetros de saída, é mais fácil detectar na chamada do código que uma variável pode mudar, por exemplo: compare fun(&x)com fun(x). No último exemplo, xpoderia ser passado por valor ou como uma ref const. Como dito acima, também permitirá que você seja aprovado nullptrse não estiver interessado na saída ...
Andreas Magnusson
2
@AndreasMagnusson: Essa convenção de ponteiro como parâmetro de saída pode dar uma falsa sensação de segurança ao lidar com ponteiros transmitidos de outra fonte. Porque nesse caso a chamada é divertida (x) e * x é modificado e a fonte não tem & x para avisá-lo.
Zan Lynx
e se a chamada de função sobreviver ao escopo do chamador? Existe a possibilidade de que o destruidor do ptr compartilhado tenha sido chamado e agora a função estará tentando acessar a memória excluída, resultando em UB
Sridhar Thiagarajan
4

Sim, toda a ideia sobre um shared_ptr <> é que várias instâncias podem conter o mesmo ponteiro bruto e a memória subjacente só será liberada quando a última instância de shared_ptr <> for destruída.

Eu evitaria um ponteiro para um shared_ptr <>, pois isso anula o propósito, pois agora você está lidando com raw_pointers novamente.

R Samuel Klatchko
fonte
2

Passar por valor em seu primeiro exemplo é seguro, mas existe um idioma melhor. Passe por referência const sempre que possível - eu diria que sim, mesmo ao lidar com ponteiros inteligentes. Seu segundo exemplo não está exatamente quebrado, mas está muito !???. Tolo, não realizar nada e derrotar parte do ponto de ponteiros inteligentes, e vai te deixar em um mundo cheio de erros de dor quando você tenta desreferenciar e modificar as coisas.

Djechlin
fonte
3
Ninguém está defendendo a passagem por ponteiro, se você quer dizer ponteiro bruto. Passar por referência se não houver contenção de propriedade é certamente idiomático. A questão é, shared_ptr<>especificamente, que você deve fazer uma cópia para realmente compartilhar a propriedade; Se você tiver uma referência ou ponteiro para um, shared_ptr<>então você não está compartilhando nada e está sujeito aos mesmos problemas de vida útil de uma referência ou ponteiro normal.
ildjarn
2
@ildjarn: Passar uma referência constante é bom neste caso. O original shared_ptrdo chamador tem a garantia de durar mais que a chamada de função, portanto, a função é segura ao usar o shared_ptr<> const &. Se for necessário armazenar o ponteiro para um momento posterior, ele precisará copiar (neste ponto, uma cópia deve ser feita, ao invés de conter uma referência), mas não há necessidade de incorrer no custo de cópia, a menos que você precise fazer isto. Gostaria de passar uma referência constante neste caso ....
David Rodríguez - dribeas
3
@David: " Passar uma referência constante está bom neste caso. " Não em qualquer API sã - por que uma API exigiria um tipo de ponteiro inteligente do qual não está aproveitando em vez de apenas tomar uma referência const normal? Isso é quase tão ruim quanto a falta de correção constante. Se o que você quer dizer é que tecnicamente não está doendo nada, então certamente concordo, mas não acho que seja a coisa certa a se fazer.
ildjarn
2
... se por outro lado, a função não precisa estender o tempo de vida do objeto, então você pode simplesmente remover o shared_ptrda interface e passar uma constreferência simples ( ) para o objeto apontado.
David Rodríguez - dribeas
3
@David: E se o seu consumidor de API não estiver usando shared_ptr<>para começar? Agora, sua API é realmente um incômodo, pois força o chamador a alterar inutilmente a semântica do tempo de vida do objeto apenas para usá-lo. Não vejo nada que valha a pena defender nisso, além de dizer "tecnicamente não está prejudicando nada". Não vejo nada polêmico sobre 'se você não precisa de propriedade compartilhada, não use shared_ptr<>'.
ildjarn
0

em sua função, DoSomething você está alterando um membro de dados de uma instância de classe, de myClass modo que o que está modificando é o objeto gerenciado (ponteiro bruto) e não o objeto (shared_ptr). O que significa que no ponto de retorno desta função todos os ponteiros compartilhados para o ponteiro bruto gerenciado verão seu membro de dados: myClass::someFieldalterado para um valor diferente.

neste caso, você está passando um objeto para uma função com a garantia de que não o está modificando (falando sobre shared_ptr não o objeto possuído).

O idioma para expressar isso é por meio de: a const ref, assim

void DoSomething(const std::shared_ptr<myClass>& arg)

Da mesma forma, você está garantindo ao usuário de sua função que não está adicionando outro proprietário à lista de proprietários do ponteiro bruto. No entanto, você manteve a possibilidade de modificar o objeto subjacente apontado pelo ponteiro bruto.

CAVEAT: O que significa que, se por algum meio alguém chamar shared_ptr::resetantes de você chamar sua função, e naquele momento for o último shared_ptr possuindo o raw_ptr, seu objeto será destruído e sua função manipulará um Ponteiro pendente para o objeto destruído. MUITO PERIGOSO!!!

organicoman
fonte