I’m using QVector to hold pointers to Objects, say FYPE*, in my program.
class TYPE {
// ....
};
const TYPE *ptrToCst;
QVector<TYPE*> qtVct;
// ...
if (qtVct.contains(ptrToCst)) { // Error!!!
// ....
}
The compiler says QVector::contains expects TYPE* instead of const TYPE* as its parameter. A const_cast operation would solve this problem. But it doesn’t make any sense to me, since the contains method should never change what the pointer points to. And the equivalent code using STL vector works as expected.
std::vector<TYPE*> stlVct;
// ...
if (std::find(stlVct.begin(), stlVct.end(), ptrToCst)) { // Ok
// ...
}
What’s the reason for this difference? Did STL treat containers that hold pointers specially, so that std::find accept pointer to const object? I guess partial template specialization was involved?
This is actually a quite interesting question about const-correctness and some non-intuitive errors, but the compiler is right in rejecting the code.
Why does is not work in Qt?
The compiler rejects the code because it would break const-correctness of the code. I don’t know the library, but I think I can safely assume that the signature of contains is
QVector<T>::contains( T const & value )[1], which in the case ofT == type*means:Now the problem is that you are trying to pass a variable of type
type const *. The problem at this point is that the compiler does not know whatcontainsdoes internally, only the promises it offers in it’s interface.containspromises not to change the pointer, but says nothing about the pointee. There is nothing in the signature inhibiting the implementation ofcontainsfrom modifying the value. Consider this similar example:Why does it allow the similar call to
std::findthen?The difference with
std::findis thatfindis a template where the different argument type have a very loose coupling. The standard library does not perform type checking on the interface, but rather on the implementation of the template. If the argument is not of the proper type, it will be picked up while instantiating the template.What this means is that the implementation will be something similar to:
The type of the iterators and the value are completely unrelated, there is no constraint there other than those imposed by the code inside the template. That means that the call will match the arguments as
Iterator == std::vector<type*>::iteratorandT = type const *, and the signature will match. Because internally value is only used to compare with*start, and the comparison oftype *andtype const * constis valid (it will convert the former to the later and then compare [2]) the code compiles perfectly.If the template had the extra restriction that the type of the second argument was
Iterator::value_type const &(this could be implemented with SFINAE) thenfindwould fail to compile with the same error.[1] Note the choice of ordering in the declaration:
type const *, rather thanconst type *. They are exactly the same for the compiler (and the experienced eye), but by always adding theconstto the right it makes it trivial to identify what is being defined as const, and to identify thatconst T &withT == type *in the argument ofcontainsis notconst type *&.[2] In the same way that you cannot bind a
type * const &to atype const *, you cannot do the equivalent with pointers,type * const *cannot be converted fromtype const *, but ifconstis added to both the pointer and the pointee types, then the conversion is fine as it guarantees not to break const correctness. That conversion (which is safe) is performed in the comparison oftype * == type const * const, where the left hand side gets two extraconst:type const * const. If it is not clear why this does not break const-correctness, drop a comment and I will provide some code here.