Como posso obter o endereço de um objeto de maneira confiável quando o operador e está sobrecarregado?

170

Considere o seguinte programa:

struct ghost
{
    // ghosts like to pretend that they don't exist
    ghost* operator&() const volatile { return 0; }
};

int main()
{
    ghost clyde;
    ghost* clydes_address = &clyde; // darn; that's not clyde's address :'( 
}

Como obtenho clydeo endereço de?

Estou procurando uma solução que funcione igualmente bem para todos os tipos de objetos. Uma solução C ++ 03 seria legal, mas também estou interessado em soluções C ++ 11. Se possível, vamos evitar qualquer comportamento específico da implementação.

Estou ciente do std::addressofmodelo de função do C ++ 11 , mas não estou interessado em usá-lo aqui: gostaria de entender como um implementador da Biblioteca Padrão pode implementar esse modelo de função.

James McNellis
fonte
41
@ jalf: Essa estratégia é aceitável, mas agora que dei um soco na cabeça de tais indivíduos, como faço para contornar seu código abominável? :-)
James McNellis
5
@jalf Uhm, às vezes você precisa sobrecarregar esse operador e retornar um objeto proxy. Embora eu não possa pensar em um exemplo agora.
Konrad Rudolph
5
@ Konrad: eu também. Se você precisar, sugiro que uma opção melhor seja repensar seu design, porque sobrecarregar esse operador causa muitos problemas. :)
jalf
2
@ Konrad: Em aproximadamente 20 anos de programação em C ++ , tentei sobrecarregar esse operador. Isso foi no começo daqueles vinte anos. Ah, e eu falhei em tornar isso utilizável. Consequentemente, a entrada de perguntas frequentes sobre sobrecarga do operador diz "O endereço unário do operador nunca deve ser sobrecarregado". Você receberá uma cerveja grátis na próxima vez que nos encontrarmos, se você puder criar um exemplo convincente de sobrecarregar esse operador. (Eu sei que você está deixando Berlim, para que eu possa com segurança oferecer este :))
SBI
5
CComPtr<>e CComQIPtr<>ter um sobrecarregadooperator&
Simon Richter

Respostas:

102

Atualização: no C ++ 11, pode-se usar em std::addressofvez de boost::addressof.


Vamos primeiro copiar o código do Boost, menos o trabalho do compilador em torno dos bits:

template<class T>
struct addr_impl_ref
{
  T & v_;

  inline addr_impl_ref( T & v ): v_( v ) {}
  inline operator T& () const { return v_; }

private:
  addr_impl_ref & operator=(const addr_impl_ref &);
};

template<class T>
struct addressof_impl
{
  static inline T * f( T & v, long ) {
    return reinterpret_cast<T*>(
        &const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
  }

  static inline T * f( T * v, int ) { return v; }
};

template<class T>
T * addressof( T & v ) {
  return addressof_impl<T>::f( addr_impl_ref<T>( v ), 0 );
}

O que acontece se passarmos uma referência à função ?

Nota: addressofnão pode ser usado com um ponteiro para funcionar

Em C ++, se void func();declarado, funcé uma referência a uma função que não aceita argumentos e não retorna resultados. Essa referência a uma função pode ser trivialmente convertida em um ponteiro para a função - de @Konstantin: De acordo com 13.3.3.2, ambos T &e T *são indistinguíveis para funções. O primeiro é uma conversão de Identidade e o segundo é a conversão de Função em Ponteiro, ambos com a classificação "Correspondência Exata" (13.3.3.1.1 tabela 9).

A referência a função passar através addr_impl_ref, existe uma ambiguidade na resolução de sobrecarga para a escolha do f, o qual é resolvido graças ao argumento manequim 0, que é um intprimeiro e pode ser promovido a um long(Conversão integral).

Assim, simplesmente retornamos o ponteiro.

O que acontece se passarmos um tipo com um operador de conversão?

Se o operador de conversão T*gerar um, teremos uma ambiguidade: para f(T&,long)uma Promoção Integral é necessária para o segundo argumento, enquanto para f(T*,int)o operador de conversão é chamado no primeiro (graças a @litb)

É addr_impl_refaí que entra em ação. O Padrão C ++ exige que uma sequência de conversão possa conter no máximo uma conversão definida pelo usuário. Ao envolver o tipo addr_impl_refe forçar o uso de uma sequência de conversão, "desabilitamos" qualquer operador de conversão fornecido com o tipo.

Assim, a f(T&,long)sobrecarga é selecionada (e a Promoção Integral é realizada).

O que acontece com qualquer outro tipo?

Assim, a f(T&,long)sobrecarga é selecionada, porque o tipo não corresponde ao T*parâmetro.

Nota: a partir das observações no arquivo sobre compatibilidade com a Borland, as matrizes não decaem para ponteiros, mas são passadas por referência.

O que acontece nessa sobrecarga?

Queremos evitar a aplicação operator&ao tipo, pois ele pode ter sido sobrecarregado.

A Norma garante que reinterpret_castpode ser usada para este trabalho (consulte a resposta de @Matteo Italia: 5.2.10 / 10).

O Boost adiciona alguns detalhes conste volatilequalificadores para evitar avisos do compilador (e use corretamente a const_castpara removê-los).

  • Transmitir T&parachar const volatile&
  • Retire o constevolatile
  • Aplique o &operador ao endereço
  • Lançar de volta para um T*

O const/ volatilemalabarismo é um pouco de magia negra, mas simplifica o trabalho (em vez de fornecer 4 sobrecargas). Observe que, uma vez que Tnão é qualificado, se passarmos a ghost const&, então T*é ghost const*, portanto os qualificadores não foram realmente perdidos.

EDIT: a sobrecarga do ponteiro é usada para ponteiro para funções, eu alterei a explicação acima um pouco. Ainda não entendo por que é necessário .

A seguinte saída de ideone resume isso, um pouco.

Matthieu M.
fonte
2
"O que acontece se passarmos um ponteiro?" parte está incorreta. Se passarmos um ponteiro para algum tipo U, o endereço da função, o tipo 'T' é inferido como 'U *' e addr_impl_ref terá duas sobrecargas: 'f (U * &, long)' e 'f (U **, int) ', obviamente o primeiro será selecionado.
Konstantin Oznobihin
@ Konstantin: certo, eu tinha pensado que as duas fsobrecargas onde funcionavam modelos, enquanto que elas são funções membro regulares de uma classe de modelo, obrigado por apontá-las. (Agora eu só preciso descobrir o que é o uso da sobrecarga, qualquer dica?)
Matthieu M.
Essa é uma ótima resposta bem explicada. Eu meio que imaginei que havia um pouco mais nisso do que apenas "passar completamente char*". Obrigado, Matthieu.
James McNellis
@ James: Eu tive muita ajuda de @Konstantin que iria atacar a minha cabeça com um pedaço de pau qualquer momento eu cometi um erro: D
Matthieu M.
3
Por que seria necessário solucionar os tipos que têm uma função de conversão? Não preferiria a correspondência exata em vez de invocar qualquer função de conversão T*? EDIT: Agora eu vejo. Seria, mas, com o 0argumento, acabaria entrecruzado , seria ambíguo.
Johannes Schaub - litb 29/06
99

Use std::addressof.

Você pode pensar nisso como fazer o seguinte nos bastidores:

  1. Reinterpretar o objeto como uma referência a char
  2. Pegue o endereço disso (não chamará sobrecarga)
  3. Transforme o ponteiro de volta em um ponteiro do seu tipo.

As implementações existentes (incluindo o Boost.Addressof) fazem exatamente isso, apenas tomando cuidado conste volatilequalificação adicionais.

Konrad Rudolph
fonte
16
Eu gosto mais dessa explicação do que a selecionada, pois pode ser facilmente entendida.
Sled
49

O truque por trás boost::addressofe a implementação fornecida por @Luc Danton depende da mágica do reinterpret_cast; o padrão declara explicitamente em §5.2.10 ¶10 que

Uma expressão lvalue do tipo T1pode ser convertida no tipo "referência a T2" se uma expressão do tipo "ponteiro para T1" puder ser explicitamente convertida no tipo "ponteiro para T2" usando a reinterpret_cast. Ou seja, uma reinterpret_cast<T&>(x)conversão de referência tem o mesmo efeito que a conversão *reinterpret_cast<T*>(&x)com os operadores internos &e *. O resultado é um lvalue que se refere ao mesmo objeto que o lvalue de origem, mas com um tipo diferente.

Agora, isso nos permite converter uma referência arbitrária de objeto para a char &(com uma qualificação cv se a referência for qualificada para cv), porque qualquer ponteiro pode ser convertido em uma (possivelmente qualificado para cv) char *. Agora que temos um char &, a sobrecarga do operador no objeto não é mais relevante e podemos obter o endereço com o &operador interno .

A implementação do boost adiciona algumas etapas para trabalhar com objetos qualificados para cv: a primeira reinterpret_casté feita para const volatile char &, caso contrário, uma conversão simples char &não funcionaria para conste / ou volatilereferências ( reinterpret_castnão é possível remover const). Em seguida, o conste volatileé removido com const_cast, o endereço é levado com &e uma final reinterpet_castpara o tipo "correto" é feita.

A const_casté necessária para remover o const/ volatileque poderia ter sido adicionado ao não-const / referências voláteis, mas ele não "prejudicar" o que era um const/ volatilereferência em primeiro lugar, porque a final reinterpret_castvai voltar a adicionar a cv-qualificação se fosse lá em primeiro lugar ( reinterpret_castnão é possível remover o constmas pode adicioná-lo).

Quanto ao restante do código addressof.hpp, parece que a maior parte é para soluções alternativas. O static inline T * f( T * v, int )parece ser necessária apenas para o compilador Borland, mas sua presença introduz a necessidade de addr_impl_ref, caso contrário, tipos de ponteiro seria pego por esta segunda sobrecarga.

Edit : as várias sobrecargas têm uma função diferente, consulte @Matthieu M. excelente resposta .

Bem, também não tenho mais certeza disso; Eu deveria investigar mais esse código, mas agora estou preparando o jantar :), vou dar uma olhada mais tarde.

Matteo Italia
fonte
A explicação de Matthieu M. sobre a passagem do ponteiro para o endereço de está incorreta. Não estrague a sua grande resposta com tais edições :)
Konstantin Oznobihin
"bom apetite", uma investigação mais aprofundada mostra que a sobrecarga é necessária para referência a funções void func(); boost::addressof(func);. No entanto, remover a sobrecarga não impede o gcc 4.3.4 de compilar o código e produzir a mesma saída, então ainda não entendo por que é necessário ter essa sobrecarga.
precisa
@ Matthieu: Parece ser um bug no gcc. De acordo com 13.3.3.2, T e T * são indistinguíveis para funções. O primeiro é uma conversão de Identidade e o segundo é a conversão de Função em Ponteiro, ambos com a classificação "Correspondência Exata" (13.3.3.1.1 tabela 9). Portanto, é necessário ter argumentos adicionais.
Konstantin Oznobihin
@ Matthieu: Apenas tentei com o gcc 4.3.4 ( ideone.com/2f34P ) e obtive ambiguidade como esperado. Você tentou funções membro sobrecarregadas, como no endereço da implementação ou nos modelos de funções livres? O último (como ideone.com/vjCRs ) resultará na sobrecarga de 'T *', devido às regras de dedução de argumentos de modelo (14.8.2.1/2).
Konstantin Oznobihin
2
@curiousguy: Por que você acha que deveria? Mencionei partes padrão específicas do C ++ que prescrevem o que o compilador deve fazer e todos os compiladores aos quais tenho acesso (incluindo, entre outros, o gcc 4.3.4, comeau-online, VC6.0-VC2010) relatam a ambiguidade da mesma forma que descrevi. Você poderia elaborar seu raciocínio sobre este caso?
Konstantin Oznobihin
11

Eu vi uma implementação de addressoffazer isso:

char* start = &reinterpret_cast<char&>(clyde);
ghost* pointer_to_clyde = reinterpret_cast<ghost*>(start);

Não me pergunte como isso é compatível!

Luc Danton
fonte
5
Legal. char*é a exceção listada para digitar regras de alias.
Filhote de cachorro
6
@DeadMG Não estou dizendo que isso não está em conformidade. Eu estou dizendo que você não deve me perguntar :)
Luc Danton
1
@DeadMG Não há problema com alias aqui. A questão é: está reinterpret_cast<char*>bem definido.
precisa
2
@curiousguy e a resposta é sim, sempre é permitido converter qualquer tipo de ponteiro [unsigned] char *e, assim, ler a representação do objeto apontado. Essa é outra área em que charhá privilégios especiais.
underscore_d
@underscore_d Só porque um elenco é "sempre permitido" não significa que você pode fazer qualquer coisa com o resultado do elenco.
Curiousguy
5

Dê uma olhada no boost :: addressof e sua implementação.

Konstantin Oznobihin
fonte
1
O código Boost, embora interessante, não explica como sua técnica funciona (nem explica por que são necessárias duas sobrecargas).
James McNellis
você quer dizer 'sobrecarga estática T * f (T * v, int)'? Parece necessário apenas para a solução alternativa do Borland C. A abordagem usada lá é bastante direta. A única coisa sutil (fora do padrão) é a conversão de 'T &' para 'char &'. Embora padrão, permita a conversão de 'T *' para 'char *', parece não haver requisitos para a conversão de referência. No entanto, pode-se esperar que funcione exatamente da mesma forma na maioria dos compiladores.
Konstantin Oznobihin
@ Konstantin: a sobrecarga é usada porque, para um ponteiro, addressofretorna o ponteiro em si. É discutível se é o que o usuário queria ou não, mas é como ele especificou.
precisa
@ Matthieu: você tem certeza? Tanto quanto eu posso dizer, qualquer tipo (incluindo tipos de ponteiro) é enrolado dentro de um addr_impl_ref, de modo a sobrecarga ponteiro nunca deve ser chamado ...
Matteo Italia
1
@ KonstantinOznobihin, isso realmente não responde à pergunta, pois tudo o que você diz é onde procurar a resposta, não qual é a resposta .