c++templates

why does operator= definition (not declaration) has to be written when suitable template is readily available


I have the following self-sufficient example of a c++ custom number to track constructions and destructions.

#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;
}

compiled using g++ main.cpp -std=c++20 with any recent compiler version.

Question

Why can't I exchange line A for line B?

Expectation

I expected that line B tells the compiler at operator resolution stage that said operator (line B) exists. Once upon the resolution stage, the compiler is then in search of a suitable definition of said operator declaration (line B) and will find it in line C.

Actual result

Instead, the compiler states undefined reference to `number::operator=(number const&)'.


Solution

  • A template function (or method) is a recipie for making functions.

    A function (or method) declaration is both instructions on how to dispatch a function call, and a promise by the developer that you'll implement it later.

    The template function

    template<class T>
    void foo(T);
    

    isn't a function; its instantiation foo<int> is not the same as:

    void foo(int);
    

    The purpose behind this rule is to allow you to use the overload mechanism to route specific cases away from the template implementation to type-specific versions.

    So your foo<T> might call convert_to_int(t), and route the work back to foo(int); and calling convert_to_int(int) might be a pessimization.

    In addition, because special members are super-special and super-magical in how they are used and in how they are instantiated, it was decided that a template can never count as an implementation of a special member function (copy construction, move construction, copy assignment and move assignment).

    This was to avoid accidentally making a class copyable just because you wrote

    template<class...Ts>
    my_class(Ts&&...ts)
    

    in order to change how your your class is "copyable", you have to create a non-template my_class(my_class const&), not rely on a template function to do it for you.

    So, (a) it wouldn't work for non-special member functions, and (b) it specifically doesn't work for special member functions as an intentional part of the standard.

    ...

    I would be extremely leery of doing what you are doing, as an aside. The semantics of what = and copying mean should generally be simple; yours is not, because a=b may or may not copy certain parts of bs state to a.

    You are also mixing business logic deep into your class.

    I would follow the rule of 0 myself. In the rule of 0, we place classes with special member functions as far away from the end user as you can.

    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;
    };
    

    this creation_count_index can now be put into another class, and it gives the semantic meaning of your index without the other class having to be aware of how it works.

    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;
    };
    

    your get_value path can coexist with the above. The point is you no longer have to mess with index at all, it "just does the right thing".