¿Por qué std :: function no participa en la resolución de sobrecarga?

17

Sé que el siguiente código no se compilará.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Porque std::functionse dice que no considera la resolución de sobrecarga, como leí en esta otra publicación .

No entiendo completamente las limitaciones técnicas que forzaron este tipo de solución.

Leí sobre las fases de traducción y plantillas en cppreference, pero no puedo pensar en ningún razonamiento para el que no pueda encontrar un contraejemplo. Explicado a un medio laico (todavía nuevo en C ++), ¿qué y durante qué etapa de traducción hace que lo anterior no se compile?

TuRtoise
fuente
1
@Evg, pero solo hay una sobrecarga que tendría sentido para ser aceptada en el ejemplo de OP. En su ejemplo, ambos harían una pareja
idclev 463035818

Respuestas:

13

Esto realmente no tiene nada que ver con las "fases de traducción". Se trata únicamente de los constructores de std::function.

Ver, std::function<R(Args)>no requiere que la función dada sea exactamente del tipo R(Args). En particular, no requiere que se le asigne un puntero de función. Puede tomar cualquier tipo invocable (puntero de función miembro, algún objeto que tenga una sobrecarga operator()) siempre que sea invocable como si tomara Argsparámetros y devuelva algo convertible R (o si Res así void, puede devolver cualquier cosa).

Para ello, el constructor adecuado de std::functiondebe ser una plantilla : template<typename F> function(F f);. Es decir, puede tomar cualquier tipo de función (sujeto a las restricciones anteriores).

La expresión bazrepresenta un conjunto de sobrecarga. Si usa esa expresión para llamar al conjunto de sobrecarga, está bien. Si usa esa expresión como parámetro de una función que toma un puntero de función específico, C ++ puede reducir el conjunto de sobrecarga a una sola llamada, lo que lo hace correcto.

Sin embargo, una vez que una función es una plantilla y está utilizando la deducción de argumentos de plantilla para descubrir cuál es ese parámetro, C ++ ya no tiene la capacidad de determinar cuál es la sobrecarga correcta en el conjunto de sobrecarga. Por lo tanto, debe especificarlo directamente.

Nicol Bolas
fuente
Disculpe si me equivoqué, pero ¿por qué C ++ no tiene la capacidad de distinguir entre sobrecargas disponibles? Ahora veo que std :: function <T> acepta cualquier tipo compatible, no solo una coincidencia exacta, sino que solo una de esas dos sobrecargas de baz () es invocable como si tomara parámetros específicos. ¿Por qué es imposible desambiguar?
TuRtoise
Porque todo lo que C ++ ve es la firma que cité. No sabe con qué se supone que debe enfrentarse. Esencialmente, cada vez que usa un conjunto de sobrecarga contra algo que no es obvio cuál es la respuesta correcta explícitamente del código C ++ (el código es esa declaración de plantilla), el lenguaje lo obliga a deletrear lo que quiere decir.
Nicol Bolas
1
@TuRtoise: el parámetro de plantilla en la functionplantilla de clase es irrelevante . Lo que importa es el parámetro de plantilla en el constructor al que está llamando. Que es solo typename F: aka, cualquier tipo.
Nicol Bolas
1
@ TuRtoise: Creo que estás malinterpretando algo. Es "solo F" porque así es como funcionan las plantillas. A partir de la firma, ese constructor de funciones toma cualquier tipo, por lo que cualquier intento de llamarlo con una deducción de argumento de plantilla deducirá el tipo del parámetro. Cualquier intento de aplicar la deducción a un conjunto de sobrecarga es un error de compilación. El compilador no intenta deducir todos los tipos posibles del conjunto para ver cuáles funcionan.
Nicol Bolas
1
"Cualquier intento de aplicar la deducción a un conjunto de sobrecarga es un error de compilación. El compilador no intenta deducir todos los tipos posibles del conjunto para ver cuáles funcionan". Exactamente lo que me faltaba, gracias :)
TuRtoise
7

La resolución de sobrecarga ocurre solo cuando (a) está llamando el nombre de una función / operador, o (b) lo está enviando a un puntero (a función o función miembro) con una firma explícita.

Tampoco está ocurriendo aquí.

std::functiontoma cualquier objeto que sea compatible con su firma. No toma un puntero de función específicamente. (una lambda no es una función estándar, y una función estándar no es una función lambda)

Ahora en mis variantes de funciones homebrew, para la firma R(Args...)también acepto un R(*)(Args...)argumento (una coincidencia exacta) exactamente por esta razón. Pero significa que eleva las firmas de "coincidencia exacta" por encima de las firmas "compatibles".

El problema central es que un conjunto de sobrecarga no es un objeto C ++. Puede nombrar un conjunto de sobrecarga, pero no puede pasarlo "nativamente".

Ahora, puede crear un conjunto de pseudo-sobrecarga de una función como esta:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

Esto crea un único objeto C ++ que puede hacer una resolución de sobrecarga en el nombre de una función.

Expandiendo las macros, obtenemos:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

lo cual es molesto de escribir. Una versión más simple, solo un poco menos útil, está aquí:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

tenemos una lambda que toma cualquier número de argumentos, luego los reenvía a ellos baz.

Entonces:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

trabajos. Diferimos la resolución de sobrecarga en el lambda que almacenamos fun, en lugar de pasar funun conjunto de sobrecarga directamente (que no puede resolver).

Ha habido al menos una propuesta para definir una operación en el lenguaje C ++ que convierte un nombre de función en un objeto de conjunto de sobrecarga. Hasta que dicha propuesta estándar esté en el estándar, la OVERLOADS_OFmacro es útil.

Podría ir un paso más allá y admitir puntero de función de conversión compatible.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

pero eso está empezando a ponerse obtuso.

Ejemplo en vivo .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Yakk - Adam Nevraumont
fuente
5

El problema aquí es que no hay nada que le diga al compilador cómo hacer la función para decaer el puntero. Si usted tiene

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Entonces el código funcionaría ya que ahora el compilador sabe qué función desea, ya que hay un tipo concreto al que está asignando.

Cuando usa std::function, llama a su constructor de objetos de función que tiene la forma

template< class F >
function( F f );

y como es una plantilla, debe deducir el tipo de objeto que se pasa. Como bazes una función sobrecargada, no hay un tipo único que pueda deducirse, por lo que la deducción de plantilla falla y se obtiene un error. Tendrías que usar

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

para forzar un solo tipo y permitir la deducción.

NathanOliver
fuente
"dado que baz es una función sobrecargada, no hay un tipo único que pueda deducirse", pero desde C ++ 14 "este constructor no participa en la resolución de sobrecarga a menos que f sea invocable para los tipos de argumento Args ... y devuelva el tipo R" Estoy No estoy seguro, pero estaba cerca de esperar que esto fuera suficiente para resolver la ambigüedad
idclev 463035818
3
@ formerlyknownas_463035818 Pero para determinar eso, primero debe deducir un tipo, y no puede, ya que es un nombre sobrecargado.
NathanOliver
1

En el momento en que el compilador decide qué sobrecarga pasar al std::functionconstructor, todo lo que sabe es que el std::functionconstructor tiene la plantilla para tomar cualquier tipo. No tiene la capacidad de probar ambas sobrecargas y descubrir que la primera no se compila, pero la segunda sí.

La forma de resolver esto es decirle explícitamente al compilador qué sobrecarga quieres con un static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Alan Birtles
fuente