c++templatesoperator-overloadingostream

C++ problem with std::ostream operator<< in template class


I wouldn't have this problem if I put the class code in the same header or same .cpp file, but when I have the class specification in the header and the class code in a separate .cpp file, I get this error when compiling main.cpp:

/usr/bin/ld: ex2.o: in function `main':
ex2.cpp:(.text+0xd0): undefined reference to `std::ostream& operator<< <int>(std::ostream&, array<int> const&)'
/usr/bin/ld: ex2.cpp:(.text+0xe6): undefined reference to `std::ostream& operator<< <float>(std::ostream&, array<float> const&)'
/usr/bin/ld: ex2.cpp:(.text+0xfc): undefined reference to `std::ostream& operator<< <double>(std::ostream&, array<double> const&)'
collect2: error: ld returned 1 exit status

I can't find a way to solve this. I even tried the accepted solution at Undefined reference to template operator with definition in same header file, but it doesn't work, I get other errors.

So, is there a way to solve this? Do I just put the class.cpp code in the header file (or include it)? Below is the code of each file.

array.h

#ifndef ARRAY_H
#define ARRAY_H

#include <ostream>

template <typename T>
class array {
    public:
        array (int size);
        array (const array<T> &ob);
        ~array ();
        
        void setElemet (T value, int pos);
        int getElement (int pos) const;
        int getSize() const;

        void arrayInit ();

        int operator[](int pos);
        array<T> &operator=(const array<T> &right);
        array<T> &operator+=(int right);
        template <typename U> friend std::ostream &operator<<(std::ostream &left, const array<U> &right);

    private:
        T *arr;
        int size;
};

#endif

array.cpp

#include <iostream>
#include "array.h"
#include <random>
using std::cout, std::endl;

template class array<int>;
template class array<float>;
template class array<double>;

template <typename T>
array<T>::array(int size) {
    this -> size = size;

    if(this -> size <= 0) {
        if(this -> size == 0)
            this -> size = 1;
        else
            this -> size *= -1;
        
        cout << "Array size corrected, current size = " << this -> size << "." << endl;
    }

    try {
        arr = new T[this -> size];
    } catch (const std::exception& ex) {
        cout << "Memory allocation failed, reason: " << ex.what() << endl;
        std::terminate();
    }

    arrayInit();
}

template <typename T>
array<T>::array(const array<T> &ob) {
    this -> size = ob.size;
    try {
        arr = new T[this -> size];
    } catch (const std::exception& ex) {
        cout << "Memory allocation failed, reason: " << ex.what() << endl;
        std::terminate();
    }

    for (int i = 0; i < size; i++) {
        arr[i] = ob.arr[i];
    }
}

template <typename T>
array<T>::~array () {
    delete[] arr;
}

template <typename T>
void array<T>::setElemet (T value, int pos) {
    if(pos < 0 || pos >= size) {
        cout << "Invalid position, array ranges from 0 to " << size - 1 << "." << endl;
    } else {
        arr[pos] = value;
    }
}

template <typename T>
int array<T>::getElement (int pos) const{
    if(pos < 0 || pos >= size) {
        cout << "Invalid position, array ranges from 0 to " << size - 1 << "." << endl;
        return -1;
    } else {
        return arr[pos];
    }
}

template <typename T>
int array<T>::getSize() const {
    return size;
}

template <typename T>
void array<T>::arrayInit (){
    std::random_device rd;
    if(std::is_same<T, int>::value) {
        std::uniform_int_distribution <int> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    } else if (std::is_same<T, float>::value) {
        std::uniform_real_distribution <float> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    } else if (std::is_same<T, double>::value) {
        std::uniform_real_distribution <double> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    }
}

template <typename T>
int array<T>::operator[](int pos) {
    try {
        if (pos < 0 || pos >= size) 
            throw pos;
        return arr[pos];
    } catch (int ex) {
        cout << "Tried to input out of array size. Array ranges from 0 to " << size - 1 << ". Position tried to input: ";
        return pos; 
    }
}

template <typename T>
array<T> &array<T>::operator=(const array<T> &right) {
    if (this == &right) return *this;

    for (int i = 0; i < size; i++) {
        arr[i] = right.arr[i];
    }

    return *this;
}

template <typename T>
array<T> &array<T>::operator+=(int right) {
    if (right <= 0) {
        cout << "Can't expand array with negative or zero input." << endl;
        return *this;
    }

    int oldSize = size;
    array<T> temp(oldSize);

    size += right;
    temp = *this;
            
    delete [] arr;

    try {
        arr = new T[size];
    } catch(const std::exception& ex) {
        cout << "Memory allocation failed, reason: " << ex.what() << endl;
        std::terminate();
    }

    for (int i = 0; i < oldSize; i++) {
        arr[i] = temp[i];
    }

    std::random_device rd;

    if(std::is_same<T, int>::value) {
        std::uniform_int_distribution <int> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    } else if (std::is_same<T, float>::value) {
        std::uniform_real_distribution <float> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    } else if (std::is_same<T, double>::value) {
        std::uniform_real_distribution <double> dist (-10000, 10000);
        for (int i = 0; i < size; i++) {
            arr[i] = dist(rd);
        }
    }
    
    return *this;
}

template <typename T>
std::ostream &operator<<(std::ostream &left, const array<T> &right) {
    if(right.size <= 0) {
        left << "Array size invalid." << "endl";
    } else {
        for(int i = 0; i < right.size; i++) {
            left << "Array element " << i << " has a value = " << right.arr[i] << "." << endl; 
        }
    }
    return left << endl;
}

ex2.cpp

#include <iostream>
#include "array.h"
using std::cout, std::cin, std::endl;

int main() {

    array<int> arr1(3);
    array<float> arr2(3);
    array<double> arr3(3);

    cout << arr1;
    cout << arr2;
    cout << arr3;

    return 0;
}

Solution

  • So thanks to @n.m.couldbeanAI I found that the solution is to place the following 3 lines to my array.cpp

    template std::ostream &operator<<(std::ostream &left, const array<int> &right);
    
    template std::ostream &operator<<(std::ostream &left, const array<float> &right);
    
    template std::ostream &operator<<(std::ostream &left, const array<double> &right);
    

    It works, but I get one warning for each (i get two other similars):

    warning: friend declaration ‘template<class U> std::ostream& operator<<(std::ostream&, const array<U>&’ is not visible to explicit specialization
       10 | template std::ostream &operator<<(std::ostream &left, const array<int> &right);
                                  ^~~~~~~~`
    
    `In file included from array.cpp:2:
    array.h:22:52: note: friend declaration here
       22 |         template <typename U> friend std::ostream &operator<<(std::ostream &left, const array<U> &right);
    

    Edit: Thanks to @ChristianStieber I moved the 3 lines at the end of the file and the code compiles without warnings.