Ordenación parcial de plantillas de función-llamada ambigua


Considere esta pieza de código de C++11:

#include <iostream>
#include <cstddef>

template<typename T> void f(T, const char*) //#1
{ 
    std::cout << "f(T, const char*)\n"; 
}

template<std::size_t N> void f(int, const char(&)[N]) //#2
{ 
    std::cout << "f(int, const char (&)[N])\n"; 
}

int main()
{
    f(7, "ab");
}

Bien, entonces... ¿qué sobrecarga se elige? Antes de derramar los granos con la salida del compilador, tratemos de razonar sobre esto.

(Todas las referencias a las secciones son para el documento estándar final para C++11, ISO/IEC 14882:2011.)

T desde #1 se deduce a int, N desde #2 se deduce a 3, ambas especializaciones son candidatos, ambos son viables, hasta ahora tan bueno. ¿Cuál es mejor?

Primero, se consideran las conversiones implícitas necesarias para hacer coincidir los argumentos de la función con los parámetros de la función. Para el primer argumento, no se necesita conversión en ningún caso ( conversión de identidad), int en todas partes, por lo que las dos funciones son igualmente buenas. Para el segundo, el tipo de argumento es const char[3], y las dos conversiones son:

  • para #1, conversión de matriz a puntero , categoría transformación de lvalue, según [13.3.3.1.1]; esta categoría de conversión se ignora al comparar secuencias de conversión de acuerdo con [13.3.3.2], por lo que esto es básicamente lo mismo que conversión de identidad para este propósito;
  • para #2, el parámetro es de tipo de referencia y se une directamente al argumento, por lo que, de acuerdo con [13.3.3.1.4], esto es de nuevo identity conversion.

De nuevo, no hay suerte: las dos funciones siguen siendo igualmente buenas. Siendo ambas especializaciones de plantilla, ahora tenemos que ver cuál la plantilla de función, si la hay, es más especializada ([14.5.6.2] y [14.8.2.4]).

EDITAR 3: La descripción a continuación es cercana, pero no del todo precisa. Vea mi respuesta para lo que creo que es la descripción correcta del proceso.

  • Deducción de argumento de plantilla con #1 como parámetro y #2 como argumento: inventamos un valor M para sustituir N, T deducido como int, const char* como parámetro se puede inicializar desde un argumento de tipo char[M], todo está bien. Por lo que puedo decir, #2 es al menos tan especializada como #1 para todos los tipos involucrados.
  • Deducción de argumento de plantilla con #2 como parámetro y #1 como argumento: inventamos un tipo U para sustituir a T, un parámetro de tipo int no se puede inicializar desde un argumento de tipo U (tipos no relacionados), un parámetro de tipo char[N] no se puede inicializar desde un argumento de tipo const char*, y el valor el parámetro N no se puede deducir de los argumentos, por lo tanto... todo falla. Por lo que puedo decir, #1 no es al menos tan especializada como #2 para todos los tipos involucrados.

EDICIÓN 1: Lo anterior ha sido editado en base a los comentarios de Columbo y dyp, para reflejar el hecho de que las referencias se eliminan antes de intentar la deducción de argumentos de plantilla en este caso.

EDITAR 2: Basado en información de hvd, los calificadores cv de nivel superior también se eliminan. En este case, significa que const char[N] se convierte en char[N], porque los calificadores cv en los elementos del array también se aplican al array en sí (un array of const es también un const array, por así decirlo); esto no era obvio en absoluto en el estándar de C++11, pero se ha aclarado para C++14.

Basado en lo anterior, diría que el orden parcial de las plantillas de funciones debe elegir #2 como más especializada, y el llamamiento debe resolver a ella sin ambigüedad.

Ahora, de vuelta a la dura realidad. GCC 4.9.1 y Clang 3.5.0, con las siguientes opciones

-Wall -Wextra -std=c++11 -pedantic 

Rechaza la llamada como ambigua, con mensajes de error similares. El error de Clang es:

prog.cc:16:2: error: call to 'f' is ambiguous
    f(7, "ab");
    ^
prog.cc:4:27: note: candidate function [with T = int] 
template<typename T> void f(T, const char*) //#1 
                         ^
prog.cc:9:30: note: candidate function [with N = 3] 
template<std::size_t N> void f(int, const char(&)[N]) //#2 
                            ^

IntelliSense de Visual C++ 2013 (basado en el compilador EDG, que yo sepa) también marca la llamada como ambigua. Curiosamente, el compilador VC++ sigue adelante y compila el código sin errores, eligiendo #2. (Yay! Está de acuerdo conmigo, así que debe ser correcto.)

La pregunta obvia para los expertos es, ¿por qué es la llamada ambigua? ¿Qué me falta (en el área de pedidos parciales, supongo)?

Author: bogdan, 2014-12-20

2 answers

Estoy publicando los detalles de mi comprensión actual del problema como una respuesta. No estoy seguro de que sea la última palabra sobre esto, pero podría servir como base para una mayor discusión si es necesario. Los comentarios de dyp, hvd y Columbo han sido esenciales para encontrar los diversos bits de información a los que se hace referencia a continuación.

Como sospechaba, el problema es con las reglas para ordenar parcialmente las plantillas de funciones. Sección [14.8.2.4] (Deducir argumentos de plantilla durante parcial ordering) dice que, después de las transformaciones preliminares que eliminan referencias y calificadores cv, la deducción de tipos se realiza como se describe en [14.8.2.5] (Deducir argumentos de plantilla de un tipo ). Esa sección es diferente de la que se refiere a las llamadas a funciones-que sería [14.8.2.1] (Deducir argumentos de plantilla de una llamada a función ).

Cuando los parámetros de plantilla se deducen de los tipos de argumentos de función, hay algunos casos especiales que se permiten; para ejemplo, un parámetro de plantilla T utilizado en un parámetro de función de tipo T* se puede deducir cuando el argumento de la función es T[i], porque la conversión de matriz a puntero está permitida en este caso. Sin embargo, este no es el proceso de deducción que se utiliza durante el ordenamiento parcial, a pesar de que todavía estamos hablando de funciones.

Supongo que la forma fácil de pensar en las reglas para la deducción de argumentos de plantilla durante el pedido parcial es decir que son las mismas reglas que para deducir argumentos de plantilla al hacer coincidir especializaciones de class template.

Claro como el barro? Tal vez un par de ejemplos ayuden.

Esto funciona, porque utiliza las reglas para deducir argumentos de plantilla de una llamada a función :

#include <iostream>
#include <type_traits>

template<typename T> void f(T*)
{
    std::cout << std::is_same<T, int>::value << '\n';
}

int main()
{
    int a[3];
    f(a);
}

E imprime 1.

Esto no lo hace, porque usa las reglas para deducir argumentos de plantilla de un tipo :

#include <iostream>

template<typename T> struct A;

template<typename T> struct A<T*>
{
    static void f() { std::cout << "specialization\n"; }
};

int main()
{
    A<int[3]>::f();
}

Y el error de Clang es

error: implicit instantiation of undefined template 'A<int [3]>'

El la especialización no se puede usar, porque T* y int[3] no coinciden en este caso, por lo que el compilador intenta crear una instancia de la plantilla primaria.

Es este segundo tipo de deducción que se utiliza durante el pedido parcial.


Volvamos a nuestras declaraciones de plantillas de función:

template<typename T> void f(T, const char*); //#1
template<std::size_t N> void f(int, const char(&)[N]); //#2

Mi descripción del proceso de pedido parcial se convierte en:

  • Deducción de argumento de plantilla con #1 como parámetro y #2 as argumento: inventamos un valor de M para sustituir N, T se deduce como int, pero un parámetro de tipo const char* no coincide con un argumento de tipo char[M], por lo que #2 es no al menos especializados, como #1 para el segundo par de tipos.
  • Deducción de argumento de plantilla con #2 como parámetro y #1 como argumento: inventamos un tipo U para sustituir T, int and U do not match (different types), a el parámetro de tipo char[N] no coincide con un argumento de tipo const char*, y el valor del parámetro de plantilla que no es de tipo N no se puede deducir de los argumentos, por lo que #1 es no al menos tan especializado como #2 para cualquier par de tipos.

Dado que, para ser elegido, una plantilla debe ser al menos tan especializada como la otra para todos los tipos, se deduce que ninguna plantilla es más especializada que la otra y la llamada es ambiguo.


La explicación anterior va en contra de la descripción de un problema similar en Core Language Active Issue 1610 (enlace proporcionado por hvd).

El ejemplo es:

template<class C> void foo(const C* val) {}
template<int N> void foo(const char (&t)[N]) {}

El autor argumenta que, intuitivamente, la segunda plantilla debe elegirse como más especializada, y que esto no sucede actualmente (ninguna plantilla es más especializada que la otra).

Luego explica que la razón es la eliminación de el calificador const de const char[N], rindiendo char[N], que hace que la deducción falle con const C* como parámetro.

Sin embargo, basado en mi entendimiento actual, la deducción fallaría en este caso, const o no const. Esto es confirmado por las implementaciones actuales en Clang y GCC: si eliminamos el calificador const de los parámetros de ambas plantillas de función y llamamos a foo() con un argumento char[3], la llamada sigue siendo ambigua. Arrays y punteros simplemente no coinciden de acuerdo con el reglas actuales durante el pedido parcial.

Dicho esto, no soy miembro del comité, por lo que puede haber más de lo que actualmente entiendo.


Actualización: Recientemente me he topado con otro problema activo que se remonta a 2003: problema 402.

El ejemplo de allí es equivalente al de 1610. Los comentarios sobre el problema dejan claro que las dos sobrecargas no están ordenadas de acuerdo con el algoritmo de ordenación parcial tal como está, precisamente debido a la falta de reglas de decaimiento de matriz a puntero durante el ordenamiento parcial.

El último comentario es:

Hubo cierto sentimiento de que sería deseable tener este caso ordenado, pero no creemos que valga la pena gastar el tiempo para trabajar en él ahora. Si nos fijamos en algunos cambios de orden parcial más grandes en algún momento, vamos a considerar esto de nuevo.

Por lo tanto, estoy bastante seguro de que la interpretación que di arriba es correcto.

 9
Author: bogdan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 13:27:15

Inicialmente, pensé que el problema con su código es que no estaba contabilizando el ajuste del tipo de función. El ajuste del tipo de función hace que una matriz con límites se interprete como un puntero al tipo. Traté de encontrar la solución a su problema preguntando al compilador lo que está viendo estáticamente a través de plantillas, pero en su lugar obtengo resultados más interesantes:

#include <iostream>
#include <type_traits>

template<typename T, std::size_t N>
void is_same( const T* _left, const char(&_right)[N] )
{
 typedef decltype(_left) LeftT;
 typedef decltype(_right) RightT;

 std::cout << std::is_same<LeftT,const char*>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[4]>::value << std::endl;
 std::cout << std::is_same<RightT,const char*>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[4]>::value << std::endl;
}

int main()
{
 std::cout << std::boolalpha;

 is_same( "ab", "cd" );

 return 0;
}

Los rendimientos de salida: verdadero falso falso falso verdadero false

El compilador es capaz de distinguir argumentos en este caso.

[2] Editar 1: Aquí hay más código. La introducción de referencias rvalue hace que las funciones sean más distinguibles.
#include <iostream>

// f
template<typename _T>
 void f( _T, const char* )
 {
  std::cout << "f( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void f( int, const char(&)[_kN] )
 {
  std::cout << "f( int, const char (&)[_kN] )" << std::endl;
 }

// g
template<typename _T>
 void g( _T, const char* )
 {
  std::cout << "g( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void g( int, const char(&&)[_kN] )
 {
  std::cout << "g( int, const char (&&)[_kN] )" << std::endl;
 }

// h
template<std::size_t _kN>
 void h( int, const char(&)[_kN] )
 {
  std::cout << "h( int, const char(&)[_kN] )" << std::endl;
 }

template<std::size_t _kN>
 void h( int, const char(&&)[_kN] )
 {
  std::cout << "h( int, const char (&&)[_kN] )" << std::endl;
 }

int main()
{
 //f( 7, "ab" ); // Error!
 //f( 7, std::move("ab") ); // Error!
 f( 7, static_cast<const char*>("ab") ); // OK
 //f( 7, static_cast<const char(&)[3]>("ab") ); // Error!
 //f( 7, static_cast<const char(&&)[3]>("ab") ); // Error!

 g( 7, "ab" ); // OK
 //g( 7, std::move("ab") ); // Error!
 g( 7, static_cast<const char*>("ab") ); // OK
 g( 7, static_cast<const char(&)[3]>("ab") ); // OK
 //g( 7, static_cast<const char (&&)[3]>("ab") ); // Error!

 h( 7, "ab" ); // OK (What? Why is this an lvalue?)
 h( 7, std::move("ab") ); // OK
 //h( 7, static_cast<const char*>("ab") ); // Error
 h( 7, static_cast<const char(&)[3]>("ab") ); // OK
 h( 7, static_cast<const char(&&)[3]>("ab") ); // OK

 return 0;
}
 0
Author: user9587,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-27 02:35:07