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) ) ) isboost :: 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).

questionAnswers(0)

yourAnswerToTheQuestion