Diferença entre std :: reference_wrapper e ponteiro simples?

99

Por que é necessário ter std::reference_wrapper? Onde deve ser usado? Como é diferente de um simples ponteiro? Como seu desempenho se compara a um simples ponteiro?

Laurynas Lazauskas
fonte
4
É basicamente um ponteiro que você usa .com em vez de->
MM
5
@MM Não, usar .não funciona da maneira que você sugere (a menos que em algum ponto a proposta de ponto da operadora seja adotada e integrada :))
Columbo
3
São questões como esta que me deixam infeliz quando tenho que trabalhar com o novo C ++.
Nils
Para acompanhar Columbo, std :: reference_wrapper é usado com sua get()função-membro ou com sua conversão implícita de volta ao tipo subjacente.
Max Barraclough

Respostas:

88

std::reference_wrapperé útil em combinação com modelos. Ele envolve um objeto armazenando um ponteiro para ele, permitindo a reatribuição e a cópia, enquanto imita sua semântica usual. Também instrui determinados modelos de biblioteca a armazenar referências em vez de objetos.

Considere os algoritmos no STL que copiam functores: Você pode evitar essa cópia simplesmente passando um wrapper de referência referindo-se ao functor em vez do próprio functor:

unsigned arr[10];
std::mt19937 myEngine;
std::generate_n( arr, 10, std::ref(myEngine) ); // Modifies myEngine's state

Isso funciona porque ...

  • ... reference_wrappersão sobrecarregadosoperator() para que possam ser chamados da mesma forma que os objetos de função aos quais se referem:

    std::ref(myEngine)() // Valid expression, modifies myEngines state
  • … (Des) como referências comuns, copiar (e atribuir) reference_wrappersapenas atribui a ponta.

    int i, j;
    auto r = std::ref(i); // r refers to i
    r = std::ref(j); // Okay; r refers to j
    r = std::cref(j); // Error: Cannot bind reference_wrapper<int> to <const int>

Copiar um invólucro de referência é praticamente equivalente a copiar um ponteiro, que é o mais barato possível. Todas as chamadas de função inerentes ao seu uso (por exemplo, aquelas paraoperator() ) devem ser apenas embutidas, pois são de uma linha.

reference_wrappers são criados via std::refestd::cref :

int i;
auto r = std::ref(i); // r is of type std::reference_wrapper<int>
auto r2 = std::cref(i); // r is of type std::reference_wrapper<const int>

O argumento template especifica o tipo e qualificação cv do objeto referido; r2refere-se a a const inte somente produzirá uma referência a const int. Chamadas para referenciar wrappers com constfunctores neles só chamarão a constfunção de membrooperator() .

Os inicializadores Rvalue não são permitidos, pois permiti-los faria mais mal do que bem. Uma vez que rvalues ​​seriam movidos de qualquer maneira (e com eliminação de cópia garantida mesmo que seja evitada parcialmente), não melhoramos a semântica; podemos introduzir ponteiros pendentes, pois um invólucro de referência não prolonga a vida útil do ponteiro.

Interação da biblioteca

Como mencionado antes, pode-se instruir make_tuplea armazenar uma referência no resultado tuplepassando o argumento correspondente por meio de reference_wrapper:

int i;
auto t1 = std::make_tuple(i); // Copies i. Type of t1 is tuple<int>
auto t2 = std::make_tuple(std::ref(i)); // Saves a reference to i.
                                        // Type of t2 is tuple<int&>

Observe que isso é um pouco diferente de forward_as_tuple: Aqui, rvalues ​​como argumentos não são permitidos.

std::bindmostra o mesmo comportamento: não copia o argumento, mas armazena uma referência se for um reference_wrapper. Útil se aquele argumento (ou o functor!) Não precisar ser copiado, mas permanecer no escopo enquanto o bind-functor é usado.

Diferença de ponteiros comuns

  • Não há nível adicional de indireção sintática. Os ponteiros devem ser referenciados para obter um valor para o objeto ao qual se referem; reference_wrappers têm um operador de conversão implícito e podem ser chamados como o objeto que envolvem.

    int i;
    int& ref = std::ref(i); // Okay
  • reference_wrappers, ao contrário dos ponteiros, não têm um estado nulo. Eles devem ser inicializados com uma referência ou outrareference_wrapper .

    std::reference_wrapper<int> r; // Invalid
  • Uma semelhança é a semântica de cópia superficial: Ponteiros e reference_wrappers podem ser reatribuídos.

Columbo
fonte
É std::make_tuple(std::ref(i));superior de std::make_tuple(&i);alguma forma?
Laurynas Lazauskas
6
@LaurynasLazauskas É diferente. O último que você mostrou salva um ponteiro i, não uma referência a ele.
Columbo
Hm ... Acho que ainda não consigo distinguir esses dois tão bem quanto gostaria ... Bem, obrigado.
Laurynas Lazauskas
@Columbo Como um array de wrappers de referência é possível se eles não têm estado nulo? Os arrays geralmente não começam com todos os elementos definidos para o estado nulo?
anatolyg
2
@anatolyg O que o impede de inicializar esse array?
Columbo
27

Existem, pelo menos, dois objetivos motivadores de std::reference_wrapper<T>:

  1. É fornecer semântica de referência para objetos passados ​​como parâmetro de valor para modelos de função. Por exemplo, você pode ter um grande objeto de função que deseja passar para o std::for_each()qual recebe seu objeto de função parâmetro por valor. Para evitar copiar o objeto, você pode usar

    std::for_each(begin, end, std::ref(fun));

    Passar argumentos std::reference_wrapper<T>para uma std::bind()expressão é bastante comum para vincular argumentos por referência em vez de por valor.

  2. Ao usar um std::reference_wrapper<T>com std::make_tuple()o elemento de tupla correspondente torna-se um em T&vez de um T:

    T object;
    f(std::make_tuple(1, std::ref(object)));
Dietmar Kühl
fonte
Você pode dar um exemplo de código para o primeiro caso?
user1708860
1
@ user1708860: você quer dizer diferente daquele fornecido ...?
Dietmar Kühl
Quero dizer, o código real que acompanha o std :: ref (fun) porque não entendo como é usado (a menos que fun seja um objeto e não uma função ...)
user1708860
2
@ user1708860: sim, provavelmente funé um objeto de função (ou seja, um objeto de uma classe com um operador de chamada de função) e não uma função: se funfor uma função real, std::ref(fun)não tem propósito e torna o código potencialmente mais lento.
Dietmar Kühl
23

Outra diferença, em termos de código reference_wrapperautodocumentado , é que o uso de um essencialmente nega a propriedade do objeto. Em contraste, um unique_ptrdeclara propriedade, enquanto um ponteiro simples pode ou não ser propriedade (não é possível saber sem olhar muitos códigos relacionados):

vector<int*> a;                    // the int values might or might not be owned
vector<unique_ptr<int>> b;         // the int values are definitely owned
vector<reference_wrapper<int>> c;  // the int values are definitely not owned
Edward Loper
fonte
3
a menos que seja um código pré-c ++ 11, o primeiro exemplo deve implicar em valores opcionais não proprietários, por exemplo, para uma pesquisa de cache baseada no índice. Seria bom se o std nos fornecesse algo padrão para representar um valor de propriedade não nulo (variantes únicas e compartilhadas)
Bwmat
Talvez não seja tão importante no C ++ 11, onde ponteiros vazios quase sempre serão valores emprestados de qualquer maneira.
Elling
reference_wrapperé superior aos ponteiros brutos não apenas porque é claro que não é proprietário, mas também porque não pode ser nullptr(sem travessuras) e, portanto, os usuários sabem que não podem passar nullptr(sem travessuras) e você sabe que não precisa verifique se há.
underscore_d
19

Você pode pensar nisso como um invólucro conveniente em torno das referências para que possa usá-las em contêineres.

std::vector<std::reference_wrapper<T>> vec; // OK - does what you want
std::vector<T&> vec2; // Nope! Will not compile

É basicamente uma CopyAssignableversão de T&. Sempre que quiser uma referência, mas tem que ser atribuível, use std::reference_wrapper<T>ou sua função auxiliar std::ref(). Ou use um ponteiro.


Outras peculiaridades sizeof::

sizeof(std::reference_wrapper<T>) == sizeof(T*) // so 8 on a 64-bit box
sizeof(T&) == sizeof(T) // so, e.g., sizeof(vector<int>&) == 24

E comparação:

int i = 42;
assert(std::ref(i) == std::ref(i)); // ok

std::string s = "hello";
assert(std::ref(s) == std::ref(s)); // compile error
Barry
fonte
1
@LaurynasLazauskas Pode-se chamar objetos de função contidos no wrapper diretamente. Isso também é explicado na minha resposta.
Columbo
2
Como a implementação de referência é apenas um ponteiro interno, não consigo entender por que os wrappers adicionam qualquer indireção ou penalidade de desempenho
Riga
4
Não deve ser mais indireto do que uma simples referência quando se trata de um código de lançamento
Riga
3
Eu esperaria que o compilador incorporasse o reference_wrappercódigo trivial , tornando-o idêntico ao código que usa um ponteiro ou referência.
David Stone
4
@LaurynasLazauskas: std::reference_wrappertem a garantia de que o objeto nunca é nulo. Considere um aluno std::vector<T *>. Você tem que examinar todo o código da classe para ver se esse objeto pode armazenar a nullptrno vetor, enquanto com std::reference_wrapper<T>, você tem a garantia de ter objetos válidos.
David Stone