У меня есть следующий самодостаточный пример пользовательского номера С++ для отслеживания построек и разрушений.
#include<iostream>
class number;
template<typename T>
double get_value(T const&);
template<typename T>
double get_value(T const& x ){ return (double)x; }
template<typename T> requires std::is_same_v<T,number>
double get_value(T const& rhs){ return rhs.get(); }
class number{
private:
const size_t index;
double value;
static size_t get_index(){ static size_t count=0; return count++; }
public:
number():
index(get_index()),
value(0)
{
std::cout<<"number_"<<index<<"::number(void).\n";
}
~number(){
std::cout<<"number_"<<index<<"::~number()\n";
}
double const& get()const{ return value; }
void print(std::ostream& ooo=std::cout)const {
ooo<<"number_"<<index<<"_"<<value<<"\n";
}
// line A:
number const& operator=(number const& rhs){
return this->operator=<number>(rhs);
};
// line B:
//number const& operator=(number const&);
template<typename T>
number(T const& rhs):
index( get_index() ),
value( get_value<T>(rhs) )
{
std::cout<<"number_"<<index<<"::number(T).\n";
}
// line C:
template<typename T>
number const& operator= (T const& rhs){
value =get_value<T>(rhs);
return *this;
}
template<typename T>
number const& operator+= (T const& rhs){
value+=get_value<T>(rhs);
return *this;
}
};
template<typename T1, typename T2>
requires std::is_same_v<T1,number> or std::is_same_v<T2,number>
number operator+(T1 const& x1, T2 const& x2){
return number(get_value(x1)+get_value(x2));
}
int main(){
number d0;
d0=0;
number d1=1;
d1=d0;
number d2=d1;
d2=d1+d2;
number d3=d1+d2;
d3=d1+2;
d3=1+d2;
}
скомпилирован с использованием g++ main.cpp -std=c++20
с любой последней версией компилятора.
Почему я не могу обменять линию А на линию Б?
Я ожидал, что строка B сообщит компилятору на этапе разрешения оператора, что указанный оператор (строка B) существует. На этапе разрешения компилятор ищет подходящее определение указанного объявления оператора (строка B) и находит его в строке C.
Вместо этого компилятор указывает undefined reference to `number::operator=(number const&)'
.
Строка C (по шаблону) не является подходящим определением для декларации в строке B (не по шаблону). А поскольку строка C является шаблоном, она в любом случае будет создана только при использовании.
@AlanBirtles Правильно ли я понимаю, что компиляция не удалась, потому что шаблон является частью подписи объекта? Я думал, что компилятор проверяет кандидатов на шаблоны и только после этого просматривает полученные подписи. Так это было мое заблуждение?
operator=(number const&)
имеет подпись, отличную от operator=<number>(number const&)
.
@user23311233 - Также шаблон может быть оператором присваивания, но не может быть специальным оператором присваивания копирования, поскольку его подпись строго регламентирована.
«У меня есть следующий самодостаточный пример специального номера C++ для отслеживания построек и разрушений». -- Не могли бы вы упростить его до самодостаточного примера класса C++, для которого «определение оператора = (а не объявление) должно быть написано, когда подходящий шаблон легко доступен»? Удалите из определения практически все, кроме строк A, B и C (соответствующим образом упрощенных до ничего не делающих функций).
«Почему я не могу обменять линию А на линию Б?» -- Может быть, лучше спросить: «Почему я не могу отказаться от строки А?» Какую выгоду вы надеетесь получить от наличия линии Б (или линии А)?
В духе упрощения, будет ли следующий код демонстрировать ваш вопрос без лишнего шума? Строка A: // void foo(int) {}
Строка B: void foo(int);
Строка C: template <class T> void foo(T) {}
Обертка: int main() {
foo(1);
}
(Извлеките соответствующие фрагменты из определения класса, позволяя удалить класс, и переименуйте operator=
во что-то общее.) Чем меньше отвлекающих факторов в вашем коде , тем лучше.
В вашем классе есть оператор присваивания копирования, либо строка A, либо B, либо, при отсутствии таковых, его сгенерировал компилятор. В вашем классе также есть оператор присваивания шаблона. При прочих равных условиях нешаблонная функция подойдет лучше, чем шаблонная. Поэтому, когда вы предоставляете только объявление назначения копии, оно выбирается компилятором, и компоновщик справедливо жалуется, что его определение не найдено.
Шаблонная функция (или метод) — это рецепт создания функций.
Объявление функции (или метода) — это одновременно инструкция о том, как отправить вызов функции, и обещание разработчика, что вы реализуете это позже.
Функция шаблона
template<class T>
void foo(T);
не является функцией; его создание foo<int>
не то же самое, что:
void foo(int);
Цель этого правила — позволить вам использовать механизм перегрузки для маршрутизации конкретных случаев из реализации шаблона в версии для конкретного типа.
Таким образом, ваш foo<T>
может вызвать convert_to_int(t)
и перенаправить работу обратно на foo(int)
; и звонок convert_to_int(int)
может быть пессимизацией.
Кроме того, поскольку специальные члены являются сверхспециальными и сверхмагическими в том, как они используются и как создаются экземпляры, было решено, что шаблон никогда не может считаться реализацией специальной функции-члена (конструкция копирования, конструкция перемещения, копирование назначения и перемещение назначения).
Это было сделано для того, чтобы избежать случайного копирования класса только потому, что вы написали
template<class...Ts>
my_class(Ts&&...ts)
Чтобы изменить способ «копирования» вашего класса, вам нужно создать нешаблон my_class(my_class const&)
, а не полагаться на функцию шаблона, которая сделает это за вас.
Итак, (а) это не будет работать для неспециальных функций-членов, и (б) это конкретно не работает для специальных функций-членов как преднамеренной части стандарта.
...
Кстати, я бы крайне осторожен с тем, что делаете вы. Семантика того, что означают =
и копирование, обычно должна быть простой; у вас нет, потому что a=b
может копировать или не копировать определенные части состояния b
в a
.
Вы также глубоко внедряете бизнес-логику в свой класс.
Я бы сам следовал правилу 0. По правилу 0 мы размещаем классы со специальными функциями-членами как можно дальше от конечного пользователя.
struct creation_count_index {
static int get_index() {
static int current = 0;
return current++;
}
const int index = get_index();
creation_count_index(creation_count_index const&){}
creation_count_index(creation_count_index &&){}
creation_count_index& operator=(creation_count_index const&)&{return *this;}
creation_count_index& operator=(creation_count_index &&)&{return *this;}
~creation_count_index()=default;
};
теперь этот creation_count_index
можно поместить в другой класс, и он придаст семантическое значение вашему индексу, причем другой класс не должен знать, как он работает.
class number{
private:
creation_count_index index;
double value;
public:
number(number const&)=default;
number(number &&)=default;
number& operator=(number const&)& =default;
number& operator=(number &&)& =default;
};
ваш путь get_value
может сосуществовать с указанным выше. Дело в том, что вам больше не нужно вообще возиться с index
, он «просто делает правильные вещи».
Я бы предложил избегать термина «функция шаблона». По крайней мере, это неоднозначно. См. stackoverflow.com/a/1117767/580083.
B — нешаблонная функция, C — шаблон, поэтому это совершенно отдельное объявление.