Considere esta cita de Plantillas C ++: La guía completa (2ª edición) :

decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                               std::forward<Args>(args)...)};
...
return ret;

Tenga en cuenta que declarar ret con auto&& no es correcto. Como un referencia, auto&& extiende la vida útil del valor devuelto hasta el final de su alcance pero no más allá de la declaración return de la llamador de la función .

El autor dice que auto&& no es apropiado para el reenvío perfecto de un valor de retorno. Sin embargo, ¿decltype(auto) tampoco forma una referencia a xvalue / lvalue? OMI, decltype(auto) sufre el mismo problema. Entonces, ¿cuál es el punto del autor?

EDITAR:

El fragmento de código anterior irá dentro de esta plantilla de función.

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
    // here
}
12
b1sub 28 feb. 2018 a las 19:10

4 respuestas

La mejor respuesta

Hay dos deducciones aquí. Uno de la expresión de retorno y uno de la expresión std::invoke. Debido a que decltype(auto) se deduce que es el tipo declarado para la identificación sin parentesco- expresión, podemos centrarnos en la deducción de la expresión std::invoke.

Citado de [dcl.type.auto.deduct] párrafo 5:

Si el marcador de posición es el decltype(auto) especificador de tipo , T será solo el marcador de posición. El tipo deducido para T se determina como se describe en [dcl.type.simple], como si e hubiera sido el operando de decltype.

Y citado del [dcl.type.simple] párrafo 4:

Para una expresión e, el tipo denotado por decltype(e) se define de la siguiente manera:

  • si e es una expresión de identificación no sintetizada que nombra un enlace estructurado ([dcl.struct.bind]), decltype(e) es el tipo referenciado como se indica en la especificación de la declaración de enlace estructurado;

  • de lo contrario, si e es una id-expresión sin paréntesis o un acceso de miembro de clase sin sintetizar, decltype(e) es el tipo de entidad nombrada por e. Si no existe tal entidad, o si e nombra un conjunto de funciones sobrecargadas, el programa está mal formado;

  • de lo contrario, si e es un valor x, decltype(e) es T&&, donde T es el tipo de e;

  • de lo contrario, si e es un valor l, decltype(e) es T&, donde T es el tipo de e;

  • de lo contrario, decltype(e) es el tipo de e.

Nota decltype(e) se deduce que es T en lugar de T&& si e es un valor elevado. Esta es la diferencia con auto&&.

Entonces, si std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...) es un prvalue, por ejemplo, el tipo de retorno de Callable no es una referencia, es decir, el retorno por valor, ret se deduce que es el mismo tipo en lugar de una referencia, que reenvía perfectamente la semántica de regresar por valor.

5
xskxzr 1 mar. 2018 a las 02:39

Tenía una pregunta similar, pero específica sobre cómo devolver correctamente ret como si llamáramos a invoke directamente en lugar de call.

En el ejemplo que muestra, call(A, B) no tiene el mismo tipo de retorno de std::invoke(A, B) por cada A y B.

Específicamente, cuando invoke devuelve un T&&, call devuelve un T&.

Puede verlo en este ejemplo (enlace de wandbox)

#include <type_traits>
#include <iostream>

struct PlainReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        return ret;
    }
};

struct ForwardReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        return std::forward<decltype(ret)>(ret);
    }
};

struct IfConstexprReturn {
    template<class F, class Arg>
    decltype(auto) operator()(F&& f, Arg&& arg) {
        decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
        if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
            return std::move(ret);
        } else {
            return ret;
        }
    }
};

template<class Impl>
void test_impl(Impl impl) {
    static_assert(std::is_same_v<int, decltype(impl([](int) -> int {return 1;}, 1))>, "Should return int if F returns int");
    int i = 1;
    static_assert(std::is_same_v<int&, decltype(impl([](int& i) -> int& {return i;}, i))>, "Should return int& if F returns int&");
    static_assert(std::is_same_v<int&&, decltype(impl([](int&& i) -> int&& { return std::move(i);}, 1))>, "Should return int&& if F returns int&&");
}

int main() {
    test_impl(PlainReturn{}); // Third assert fails: returns int& instead
    test_impl(ForwardReturn{}); // First assert fails: returns int& instead
    test_impl(IfConstexprReturn{}); // Ok
}

Parece que la única forma de reenviar correctamente el valor de retorno de una función es haciendo

decltype(auto) operator()(F&& f, Arg&& arg) {
    decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
    if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
        return std::move(ret);
    } else {
        return ret;
    }
}

Este es un gran obstáculo (¡lo descubrí al caer en él!).

Las funciones que devuelven T&& son lo suficientemente raras que pueden pasar desapercibidas fácilmente por un tiempo.

0
Makers_F 8 abr. 2020 a las 14:10

auto&& es siempre un tipo de referencia. Por otro lado, decltype(auto) puede ser una referencia o un tipo de valor, dependiendo del inicializador utilizado.

Dado que ret en la declaración return no está entre paréntesis, el tipo de retorno deducido de call() solo depende del tipo declarado de la entidad ret, y no en la categoría de valor de la expresión ret:

template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
   decltype(auto) ret{std::invoke(std::forward<Callable>(op),
                                  std::forward<Args>(args)...)};
   ...
   return ret;
}

Si Callable devuelve por valor , la categoría de valor de la expresión de llamada de op será un valor . En ese caso:

  • decltype(auto) deducirá res como un tipo sin referencia (es decir, tipo de valor).
  • auto&& deduciría res como un tipo de referencia.

Como se explicó anteriormente, el tipo de retorno de decltype(auto) en call() simplemente da como resultado el mismo tipo que res. Por lo tanto, si auto&& se hubiera utilizado para deducir el tipo de res en lugar de decltype(auto), el tipo de retorno de call() habría sido una referencia al objeto local {{ X7}}, que no existe después de que call() regrese.

1
眠りネロク 19 may. 2019 a las 16:05

Sin embargo, ¿decltype (auto) también forma una referencia a xvalue / lvalue?

No.

Parte de la magia de decltype(auto) es que sabe que ret es un valor, por lo que no formará una referencia.

Si hubiera escrito return (ret), se habría resuelto a un tipo de referencia y estaría devolviendo una referencia a una variable local.

tl; dr: decltype(auto) no siempre es lo mismo que auto&&.

4
Lightness Races in Orbit 28 feb. 2018 a las 16:56