Semantics for wrapped objects: referência / valor por padrão via std :: move / std :: ref
Nos últimos tempos eu estou usando muitas vezes um idioma natural que eu "descobri" em C ++ 11 que é que o objeto envolvido pode automaticamente manter a referência quando isso é possível. A questão principal aqui será sobre a comparação com o comportamento desse "idioma" para outros comportamentos no padrão (veja abaixo).
Por exemplo:
template<class T>
struct wrap{
T t;
};
template<class T> wrap<T> make_wrap(T&& t){
return wrap{std::forward<T>(t)};
}
Desta forma, para o código
double a = 3.14
double const c = 3.14
Eu recebo,
typeid( make_wrap(3.14) ) --> wrap<double>
typeid( make_wrap(a) ) --> wrap<double&>
typeid( make_wrap(c) ) --> wrap<double const&>
que se eu for cuidadoso (com referências pendentes) eu posso lidar muito bem. E se eu quiserevitar referências que eu faço:
typeid( make_wrap(std::move(a)) ) --> wrap<double> // noref
typeid( make_wrap(std::move(c)) ) --> wrap<double const> // noref
Então, esse comportamento parece natural em C ++ 11.
Então voltei parastd::pair
estd::make_pair
e de alguma forma eu esperava que eles usassem esse novo comportamento aparentemente natural, mas aparentemente o comportamento é "mais tradicional". Então, por exemplo:
typeid( std::make_pair(3.14, 3.14) ) --> std::pair<double, double>
typeid( std::make_pair(a, a) ) --> std::pair<double, double> // noref
typeid( std::make_pair(c, c) ) --> std::pair<double, double> // noref
epara referências:
typeid( std::make_pair(std::ref(a), std::ref(a) ) ) --> std::pair<double&, double&> // ref
typeid( std::make_pair(std::ref(c), std::ref(c) ) ) --> std::pair<double const&, double const&> // const ref
Isso está documentado aqui:http://en.cppreference.com/w/cpp/utility/pair/make_pair
Como você vê os dois comportamentos são "opostos", em algum sentidostd::ref
é o complemento parastd::move
. Então, ambos os comportamentos são igualmente flexíveis no final, mas parece-me que ostd::make_pair
comportamento é mais difícil de implementar e manter.
A questão é:É o comportamento atual destd::make_pair
de descartar referências por padrão apenas um problema de compatibilidade com versões anteriores? porque alguma expectativa histórica? ou há uma razão mais profunda que ainda existe no C ++ 11?
Assim, parece que issostd::make_pair
comportamento é muito mais difícil de implementar, pois requer especialização parastd::ref
(std::reference_wrapper
) estd::decay
e até parece não natural (na presença de "movimento C + + 11"). Ao mesmo tempo, mesmo que eu decida continuar usando o primeiro comportamento, tenho medo de que o comportamento seja bem inesperado em relação aos padrões atuais, mesmo em C ++ 11.
Na verdade, eu gosto muito do primeiro comportamento, a ponto de a solução elegante talvez mudar o prefixomake_something
para algo comoconstruct_something
a fim de marcar a diferença de comportamento. (EDITAR: um dos comentários sugeridos para olharstd::forward_as_tuple
, então outra convenção de nomes poderia serforward_as_something
). Em relação à nomenclatura, a situação não é clara quando o valor de passagem por passagem, pass-by-ref, é misturado na construção do objeto.
EDIT2: Esta é uma edição apenas para responder a um @ Yakk sobre poder "copiar" o objeto wrap com diferentes propriedades ref / value. Isso não faz parte da questão e é apenas um código experimental:
template<class T>
struct wrap{
T t;
// "generalized constructor"? // I could be overlooking something important here
template<class T1 = T> wrap(wrap<T1> const& w) : t(std::move(w.t)){}
wrap(T&& t_) : t(std::move(t)){} // unfortunately I now have to define an explicit constructor
};
Isso parece permitir que eu copie entre tipos não relacionadoswrap<T&>
ewrap<T>
:
auto mw = make_wrap(a);
wrap<double const&> copy0 =mw;
wrap<double&> copy1 = mw; //would be an error if `a` is `const double`, ok
wrap<double> copy2 = mw;
EDIT3: Esta edição é para adicionar um exemplo concreto em que a dedução de referência tradicional pode falhar depende de um "protocolo". O exemplo é baseado no uso do Boost.Fusion.
Descobri o quanto a conversão implícita da referência ao valor pode depender da convenção. Por exemplo, o bom e velho Boost.Fusion segue a convenção STL de
As funções de geração do Fusion (por exemplo, make_list), por padrão, armazenam os tipos de elementos como tipos simples de não referência.
No entanto, que se baseia no "tipo" exato que marca a referência, no caso do Fusion foi oboost::ref
e no caso demake_pair
é...std::ref
, uma classe completamente não relacionada. Então, atualmente, dado
double a;
o tipo deboost::fusion::make_vector(5., a )
éboost::fusion::vector2<double, double>
. Ok, tudo bem.
E o tipo deboost::fusion::make_vector(5., boost::ref(a) ) ) is
boost :: fusão :: vector2`. Ok, conforme documentado.
No entanto, surpresa, como o Boost.Fusion não foi escrito com o C ++ 11 STL em mente, obtemos:boost::fusion::make_vector(5., std::ref(a) ) )
é do tipoboost::fusion::vector2<double, std::reference_wrapper<double const> >
. Surpresa!
Esta seção foi para mostrar que o comportamento atual do STL depende de um protocolo (por exemplo, que classe usar para marcar referências), enquanto o outro (o que eu chamo de comportamento "natural") usandostd::move
(ou mais exatamente a conversão de rvalue) não depende de um protocolo, mas é mais nativo da linguagem (atual).