Strange behavior of templated operator<<

2020-03-31 09:30发布

问题:

I cant understand a behavior of operator<< in my class:

header:

#ifndef VECTOR_H_
#define VECTOR_H_

#include <string>
#include <iostream>

template<class T>
class Vector {
        static const int EXPANDER = 10;
        T* array;
        int next;
        int length;
        void expand();
        void contract();
    public:
        Vector();
        Vector(const Vector& v);
        void add(const T e);
        T get(int index) const;
        bool removeByIndex(int index);
        bool remove(T e);
        int size() const;

        T operator[](int i) const;
        T& operator+=(const T& t);
        T operator+(const T& s);

        friend std::ostream& operator<< (std::ostream& os, const Vector<T>& obj);
        friend std::istream& operator>> (std::istream& is, Vector<T>& obj);

        std::string toString();
        ~Vector();
};

#endif /* VECTOR_H_ */

vector.cpp

#include "Vector.h"
#include <string>
#include <sstream>

template<class T>
Vector<T>::Vector() {
    length = EXPANDER;
    next = 0;
    array = new T[EXPANDER];
}

template<class T>
Vector<T>::Vector(const Vector& v) {
    length = v.next + 1 + EXPANDER;
    next = v.next;
    array = new T[length];
    for (int i = 0; i <= v.next; i++) {
        array[i] = v.array[i];
    }
}

template<class T>
void Vector<T>::add(const T e) {
    if (next >= length - 1)
        expand();
    array[next++] = e;
}

template<class T>
T Vector<T>::get(int index) const {
    if (index > next)
        return -1;
    return array[index - 1];
}

template<class T>
bool Vector<T>::removeByIndex(int index) {
    if (index > next)
        return false;
    for (int i = index; i < length; i++) {
        array[i] = array[i + 1];
    }
    next--;
    contract();
    return true;
}

template<class T>
bool Vector<T>::remove(T e) {
    int index = -1;
    for (int i = 0; i < next; i++) {
        if (array[i] == e) {
            index = i;
            break;
        }
    }
    if (index == -1)
        return false;
    return removeByIndex(index);
}

template<class T>
int Vector<T>::size() const {
    return next;
}

template<class T>
void Vector<T>::expand() {
    length += EXPANDER;
    T* temp = new T[length];
    for (int i = 0; i < next; i++) {
        temp[i] = array[i];
    }
    delete[] array;
    array = temp;
}

template<class T>
void Vector<T>::contract() {
    if (next + EXPANDER >= length)
        return; // NO need to contract

    length = next + EXPANDER + 1;
    T* temp = new T[length];
    for (int i = 0; i < next; i++) {
        temp[i] = array[i];
    }
    delete[] array;
    array = temp;
}

template<class T>
T Vector<T>::operator[](int i) const {
    return get(i);
}

template<class T>
T& Vector<T>::operator+=(const T& t) {
    for (int i = 0; i < t.size(); i++) {
        add(t.get(i));
    }
    return *this;
}

template<class T>
T Vector<T>::operator+(const T& s) {
    this += s;
    return this;
}

template<class T>
std::ostream& operator<< (std::ostream& os, Vector<T>& obj) {
    os << obj.toString();
    return os;
}

template<class T>
std::istream& operator>> (std::istream& is, Vector<T>& obj) {
    int size;
    T temp;
    is >> size;
    for (int i = 0; i < size; i++) {
        is >> temp;
        add(temp);
    }
    return is;
}

template<class T>
std::string Vector<T>::toString() {
    using namespace std;
    ostringstream sb;
    sb << "Elements(" << size() << "): [";
    for (int i = 0; i < next; i++) {
        sb << array[i] << ", ";
    }
    string r;
    r = sb.str();
    r = r.substr(0, r.size() - 2) + string("]");
    return r;
}

template<class T>
Vector<T>::~Vector() {}

and i run this code with main.cpp

#include "Vector.h"
#include "Vector.cpp"
#include <string>
#include <iostream>
using namespace std;
int main() {
    Vector<int> v;
    v.add(1);
    v.add(2);
    cout << v << endl;
}

the magic is in operator<< declaration in header. if i delete CONST modifier, compiler says: Undefined reference to operator<<, but with const it works. It is interesting that in my implementation, in cpp, I doesn't have CONST.

btw, how to solve warnings with warning: friend declaration declares a non-template function for operators?

回答1:

You should learn how to boil this down to a Short, Self-Contained, Compilable Example aka Minimal Working Example.

Here's an SSCCE that demonstrates the problem:

#include <iostream>

template<class T>
class Vector
{
private:
    T m;

public:
    Vector(T p) : m(p) {}

    friend std::ostream& operator<<(std::ostream& o, Vector<T> const& v);

    T get() const { return m; }
};

template<class T>
std::ostream& operator<<(std::ostream& o, Vector<T>& v)
{
    // accessing a private member leads to a compiler error here:
    return o << "[function template]" << /*v.m*/ v.get();
}

// remove this function to get the same behaviour as in the OP
std::ostream& operator<<(std::ostream& o, Vector<int> const& v)
{
    return o << "function" << v.m;
}

int main()
{
    Vector<int> v(42);
    std::cout << v;
}

Note that it's only about 30 lines long and fits onto one screen w/o scroll bars.


Now, the problem is based upon the friend declaration inside your class template:

friend std::ostream& operator<<(std::ostream& o, Vector<T> const& v);

This looks up a function named operator<< in the surrounding scopes, to befriend this already existing function. But it doesn't find any that matches those parameter type. Therefore, it declares a new function in the surrounding (= global) namespace. This function looks like this:

std::ostream& operator<<(std::ostream& o, Vector<T> const& v);

(in the global namespace) Note: it can only be found via Argument-Dependent Lookup if it's only declared via a friend-declaration.

Now, you later declare a function template of the same name. But the compiler cannot know that you meant to befriend this function template when you wrote the friend declaration inside you class template before. So those two, the friend function and the function template, are unrelated.

What happens now is the usual overload resolution. The function template is preferred if you don't add a const, since you call it with a non-const argument:

Vector<int> v;
v.add(1);
v.add(2);
cout << v << endl; // v is not const

for this argument of type Vector<int>, the binding to Vector<int>& of the function template (specialization) is preferred over the binding to the Vector<int> const& of the friend function. Hence, the function template is chosen, which has a definition (function body) and everything compiles, links and works. Note that the function template is not befriended, but this doesn't raise an error since you don't use any private members.

Once you add the const to the function template, the function template is no longer a better match for the argument. As we have a function and a function template with the same overload "rank", the non-template is preferred. Ergo, the friend function is called, which has no definition => a linker error occurs.


The simplest solution is to define the friend function inside the class definition:

template<class T>
class Vector
{
private:
    T m;

public:
    Vector(T p) : m(p) {}

    friend std::ostream& operator<<(std::ostream& o, Vector<T> const& v)
    {
        return o << v.m;
    }
};

A solution using forward-declarations:

template<class T>
class Vector;

template<class T>
std::ostream& operator<<(std::ostream& o, Vector<T> const& v);

template<class T>
class Vector
{
private:
    T m;

public:
    Vector(T p) : m(p) {}

    friend std::ostream& operator<< <T>(std::ostream& o, Vector<T> const& v);
};

template<class T>
std::ostream& operator<<(std::ostream& o, Vector<T> const& v)
{
    return o << v.m;
}

Now, the compiler can find the forward-declared function template and befriend this existing function (the specialization of the function template) instead of declaring a new function.


A solution befriending the whole function template:

template<class T>
class Vector
{
private:
    T m;

public:
    Vector(T p) : m(p) {}

    template<class U>
    friend std::ostream& operator<<(std::ostream& o, Vector<U> const& v);
};

template<class T>
std::ostream& operator<<(std::ostream& o, Vector<T> const& v)
{
    return o << v.m;
}

In this solution, the friend-declaration declares a function template, and the later declaration at namespace scope following the class' definition redeclares this function template.