ошибка разрешения функции-члена шаблона при объявлении const

c #function #templates #lambda #member

#c #функция #шаблоны #лямбда #Участник

Вопрос:

приведенный ниже код демонстрирует интересное поведение:

 #include <iostream>
using namespace std;
template<class T>
class B{
public:
  void foo(B<T> amp;x)const;
  template<class F> void foo(F f);
};
template<typename T> void B<T>::foo(B<T> amp;x)const{cout<<"foo_B"<<endl;}
template<typename T> template<typename F> void B<T>::foo(F f){cout<<"foo_F"<<endl;}
int main(){
  B<int> a;
  B<int> b;
  b.foo(a);
  b.foo([](){;});
  return(0);
}
 

мой ожидаемый результат

 foo_B
foo_F
 

но фактический результат

 foo_F
foo_F
 

это зависит от void foo(B<T> amp;x) того, объявлено ли const . Если const опущено, результат соответствует ожидаемому.

Кроме того, if const добавляется к void foo(F f) выводу, как и ожидалось.

Однако, void foo(B<T> amp;x) не изменит this , тогда void foo(F f) как изменится this . Таким образом, требуется текущий макет.

Любая идея, как решить эту проблему без удаления const , очень ценится.

Ответ №1:

Проблема здесь в том, что, поскольку void foo(B<T> amp;x)const; имеет значение const, оно должно было бы определять const для объекта, для которого вы вызываете функцию. Это не так точно, как template<class F> void foo(F f); обеспечивает соответствие, поскольку ему не нужно выполнять эту квалификацию const. Вот почему он используется для обоих вызовов.

Вы можете исправить это, также указав const для версии шаблона, например:

 #include <iostream>
using namespace std;
template<class T>
class B{
public:
  void foo(B<T> amp;x)const;
  template<class F> void foo(F f)const;
};
template<typename T> void B<T>::foo(B<T> amp;x)const{cout<<"foo_B"<<endl;}
template<typename T> template<typename F> void B<T>::foo(F f)const{cout<<"foo_F"<<endl;}
int main(){
  B<int> a;
  B<int> b;
  b.foo(a);
  b.foo([](){;});
  return(0);
}
 

Который будет печатать

 foo_B
foo_F
 

Другим вариантом было бы использовать SFINAE, чтобы ограничить версию шаблона от исключения B<T> ‘s. Это будет выглядеть так

 #include <iostream>
using namespace std;
template<class T>
class B{
public:
  void foo(B<T> amp;x)const;
  template<class F, std::enable_if_t<!std::is_same_v<B<T>, F>, bool> = true> 
  void foo(F f);
};
template<typename T> void B<T>::foo(B<T> amp;x)const{cout<<"foo_B"<<endl;}
template<typename T> template<class F, std::enable_if_t<!std::is_same_v<B<T>, F>, bool>>  
void B<T>::foo(F f){cout<<"foo_F"<<endl;}
int main(){
  B<int> a;
  B<int> b;
  b.foo(a);
  b.foo([](){;});
  return(0);
}
 

и имеет тот же результат, что и в первом примере.

Комментарии:

1. Привет. Спасибо за ответ. но этот подход не решит проблему, поскольку foo(F f) должен иметь возможность изменять объект, что было бы невозможно, если бы оно было объявлено const .

2. @user1407220 Я обновил ответ версией, которая позволит это.

3. @user1407220 Просто чтобы подчеркнуть разницу между моим подходом, поскольку они тонкие: это вызовет foo_F перегрузку для аргумента другой специализации B , который может быть или не быть тем, что нужно OP. Например. изменение типа a в последнем примере выше на, например B<double> a; , вызовет foo_F перегрузку для b.foo(a); .

4. @dfrib При значении true, если вы сделали то же самое, ваш код не будет компилироваться, поскольку void foo(B<T> amp;x) не является шаблоном функции. Вот почему я сделал это таким образом. Неясно, какого именно поведения хочет OP.

5. @NathanOliver Да, верно.

Ответ №2:

Преобразование квалификации (здесь: для неявного параметра объекта) не является преобразованием идентификатора и имеет стоимость в рейтинге разрешения перегрузки

Функция void foo(B<T>amp; x) const const -член имеет значение — , тогда как функция-член шаблона template<class F> void foo(F f) — нет. Это означает, что последнее лучше подходит для вызова, в котором неявный параметр объекта не является const, согласно [over.ics.rank]/3.2.5:

Стандартная последовательность преобразования S1 является лучшей последовательностью преобразования, чем стандартная последовательность преобразования S2, если

  • […], или, если не так, S1 и S2 отличаются только своим квалификационным преобразованием ([conv.qual]) и дают аналогичные типы T1 и T2 соответственно, где T1 может быть преобразован в T2 с помощью квалификационного преобразования.

и [over.match.best]/2.1

Если вы const -квалифицируете автоматическую переменную b в main, будет выбрана перегрузка без шаблона:

 // ^^^ `foo` overloads as in OP's example
B<int> a{};
B<int> const b{}
b.foo(a);  // foo_B
 

Если вы вместо const этого измените качество функции-члена шаблона foo , то не шаблонная функция будет совпадать с перегрузкой шаблона (требуется постоянная квалификация для неявного параметра объекта), и в этом случае не шаблонная функция выбирается как наилучшая жизнеспособная перегрузка, согласно [over.match.best]/2.4.


Если вы никогда не хотите, чтобы конкретная перегрузка участвовала в разрешении перегрузки для предиката типа: удалите ее

Однако b не может быть объявлен const в реальном приложении, что сводится к созданию копии, скажем, const c(b); а затем используйте c.foo(a)

Вы могли бы использовать признак для удаления функции-члена шаблона, если аргумент шаблона для его параметра типа template является специализацией шаблона B класса:

 #include <iostream>
#include <type_traits>

template <class T, template <class...> class Primary>
struct is_specialization_of : std::false_type {};
template <template <class...> class Primary, class... Args>
struct is_specialization_of<Primary<Args...>, Primary> : std::true_type {};
template <class T, template <class...> class Primary>
inline constexpr bool is_specialization_of_v{is_specialization_of<T, Primary>::value};

template <class T> class B {
public:
  void foo(B<T> amp;x) const { std::cout << "foo_B" << std::endl; }
  template <class F, typename = std::enable_if_t<!is_specialization_of_v<F, B>>>
  void foo(F f) {
    std::cout << "foo_F" << std::endl;
  }
};

int main() {
  B<int> a;
  B<int> b;
  b.foo(a);
  b.foo([]() { ; });
  return (0);
}
 

где мы использовали is_specialization_of особенность P2098R1 (просто обратите внимание, что это имеет отклонения в реализации для аргументов шаблона, которые являются шаблонами псевдонимов — несколько недоопределенный IIRC).

Обратите внимание, что при таком подходе ни одна из перегрузок не будет жизнеспособной для аргумента, который является другой специализацией B (чем у неявного параметра объекта).

Комментарии:

1. Привет. конечно, подход. Однако b не может быть объявлено const в реальном приложении, что сводится к созданию копии, скажем const c(b); , а затем использованию c.foo(a) . Поскольку обе функции могут существенно отличаться по своему телу, забывание последней (то есть использование b.foo(a) вместо нее) является гарантией segfault . Лучшим решением была бы структура, в которой компилятор поднимал бы флаг.

2. @user1407220 Я расширил с помощью SFINAE-подхода, чтобы удалить перегрузку шаблона, когда аргумент шаблона для него является специализацией B .

Ответ №3:

Довольно простой, даже если немного подробный способ — привести объект к const, когда вы хотите вызвать версию const:

 static_cast<const B<int>>(b).foo(a);
 

Этого достаточно для отображения вызова foo_B