Working with C++20's concepts I noticed that std::unique_ptr appears to fail to satisfy the std::equality_comparable_with<std::nullptr_t,...> concept. From std::unique_ptr's definition, it is supposed to implement the following when in C++20:
template<class T1, class D1, class T2, class D2>
bool operator==(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);
template <class T, class D>
bool operator==(const unique_ptr<T, D>& x, std::nullptr_t) noexcept;
This requirement should implement symmetric comparison with nullptr -- which from my understanding is sufficient for satisfying equality_comparable_with.
Curiously, this issue appears to be consistent on all the major compilers. The following code is rejected from Clang, GCC, and MSVC:
// fails on all three compilers
static_assert(std::equality_comparable_with<std::unique_ptr<int>,std::nullptr_t>);
Try Online
However the same assertion with std::shared_ptr is accepted:
// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);
Try Online
Unless I'm misunderstanding something, this appears to be a bug.
My question is whether this is a coincidental bug in the three compiler implementations, or is this a defect in the C++20 standard?
Note: I'm tagging this language-lawyer in case this happens to be a defect.
TL;DR: std::equality_comparable_with<T, U> requires that both T and U are convertible to the common reference of T and U. For the case of std::unique_ptr<T> and std::nullptr_t, this requires that std::unique_ptr<T> is copy-constructible, which it is not.
Buckle in. This is quite the ride. Consider me nerd-sniped.
Why don't we satisfy the concept?
std::equality_comparable_with requires:
template <class T, class U>
concept equality_comparable_with =
std::equality_comparable<T> &&
std::equality_comparable<U> &&
std::common_reference_with<
const std::remove_reference_t<T>&,
const std::remove_reference_t<U>&> &&
std::equality_comparable<
std::common_reference_t<
const std::remove_reference_t<T>&,
const std::remove_reference_t<U>&>> &&
__WeaklyEqualityComparableWith<T, U>;
That's a mouthful. Breaking apart the concept into its parts, std::equality_comparable_with<std::unique_ptr<int>, std::nullptr_t> fails for std::common_reference_with<const std::unique_ptr<int>&, const std::nullptr_t&>:
<source>:6:20: note: constraints not satisfied
In file included from <source>:1:
/…/concepts:72:13: required for the satisfaction of
'convertible_to<_Tp, typename std::common_reference<_Tp1, _Tp2>::type>'
[with _Tp = const std::unique_ptr<int, std::default_delete<int> >&; _Tp2 = const std::nullptr_t&; _Tp1 = const std::unique_ptr<int, std::default_delete<int> >&]
/…/concepts:72:30: note: the expression 'is_convertible_v<_From, _To>
[with _From = const std::unique_ptr<int, std::default_delete<int> >&; _To = std::unique_ptr<int, std::default_delete<int> >]' evaluated to 'false'
72 | concept convertible_to = is_convertible_v<_From, _To>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
(edited for legibility) Compiler Explorer link.
std::common_reference_with requires:
template < class T, class U >
concept common_reference_with =
std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> &&
std::convertible_to<T, std::common_reference_t<T, U>> &&
std::convertible_to<U, std::common_reference_t<T, U>>;
std::common_reference_t<const std::unique_ptr<int>&, const std::nullptr_t&> is std::unique_ptr<int> (see compiler explorer link).
Putting this together, there is a transitive requirement that std::convertible_to<const std::unique_ptr<int>&, std::unique_ptr<int>>, which is equivalent to requiring that std::unique_ptr<int> is copy-constructible.
Why is the std::common_reference_t not a reference?
Why is std::common_reference_t<const std::unique_ptr<T>&, const std::nullptr_t&> = std::unique_ptr<T> instead of const std::unique_ptr<T>&? The documentation for std::common_reference_t for two types (sizeof...(T) is two) says:
If T1 and T2 are both reference types, and the simple common reference type S of T1 and T2 (as defined below) exists, then the
member type type names S;
Otherwise, if std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type exists, where TiQ is a unary
alias template such that TiQ<U> is U with the addition of Ti's cv- and
reference qualifiers, then the member type type names that type;
Otherwise, if decltype(false? val<T1>() : val<T2>()), where val is a function template template<class T> T val();, is a valid type, then
the member type type names that type;
Otherwise, if std::common_type_t<T1, T2> is a valid type, then the member type type names that type;
Otherwise, there is no member type.
const std::unique_ptr<T>& and const std::nullptr_t& don't have a simple common reference type, since the references are not immediately convertible to a common base type (i.e. false ? crefUPtr : crefNullptrT is ill-formed). There is no std::basic_common_reference specialization for std::unique_ptr<T>. The third option also fails, but we trigger std::common_type_t<const std::unique_ptr<T>&, const std::nullptr_t&>.
For std::common_type, std::common_type<const std::unique_ptr<T>&, const std::nullptr_t&> = std::common_type<std::unique_ptr<T>, std::nullptr_t>, because:
If applying std::decay to at least one of T1 and T2 produces a
different type, the member type names the same type as
std::common_type<std::decay<T1>::type, std::decay<T2>::type>::type, if
it exists; if not, there is no member type.
std::common_type<std::unique_ptr<T>, std::nullptr_t> does in fact exist; it is std::unique_ptr<T>. This is why the reference gets stripped.
Can we fix the standard to support cases like this?
This has turned into P2404, which proposes changes to std::equality_comparable_with, std::totally_ordered_with, and std::three_way_comparable_with to support move-only types.
Why do we even have these common-reference requirements?
In Does `equality_comparable_with` need to require `common_reference`?, the justification given by T.C. (originally sourced from n3351 pages 15-16) for the common-reference requirements on equality_comparable_with is:
[W]hat does it even mean for two values of different types to be equal? The design says that cross-type equality is defined by mapping them to the common (reference) type (this conversion is required to preserve the value).
Just requiring the == operations that might naively be expected of the concept doesn't work, because:
[I]t allows having t == u and t2 == u but t != t2
So the common-reference requirements are there for mathematical soundness, simultaneously allowing for a possible implementation of:
using common_ref_t = std::common_reference_t<const Lhs&, const Rhs&>;
common_ref_t lhs = lhs_;
common_ref_t rhs = rhs_;
return lhs == rhs;
With the C++0X concepts that n3351 supported, this implementation would actually be used as a fallback if there was no heterogeneous operator==(T, U).
With C++20 concepts, we require a heterogeneous operator==(T, U) to exist, so this implementation will never be used.
Note that n3351 expresses that this kind of heterogeneous equality is already an extension of equality, which is only rigorously mathematically defined within a single type. Indeed, when we write heterogeneous equality operations, we are pretending that the two types share a common super-type, with the operation happening inside that common type.
Can the common-reference requirements support this case?
Perhaps the common-reference requirements for std::equality_comparable are too strict. Importantly, the mathematical requirement is only that there exists a common supertype in which this lifted operator== is an equality, but what the common reference requirements require is something stricter, additionally requiring:
The common supertype must be the one acquired through std::common_reference_t.
We must be able to form a common supertype reference to both types.
Relaxing the first point is basically just providing an explicit customization point for std::equality_comparable_with in which you could explicitly opt-in a pair of types to meet the concept. For the second point, mathematically, a "reference" is meaningless. As such, this second point can also be relaxed to allow the common supertype to be implicitly convertible from both types.
Can we relax the common-reference requirements to more closely follow the intended common-supertype requirements?
This is tricky to get right. Importantly, we actually only care that the common supertype exists, but we never actually need to use it in the code. As such, we do not need to worry about efficiency or even whether the implementation would be impossible when codifying a common supertype conversion.
This can be accomplished by changing the std::common_reference_with part of equality_comparable_with:
template <class T, class U>
concept equality_comparable_with =
__WeaklyEqualityComparableWith<T, U> &&
std::equality_comparable<T> &&
std::equality_comparable<U> &&
std::equality_comparable<
std::common_reference_t<
const std::remove_reference_t<T>&,
const std::remove_reference_t<U>&>> &&
__CommonSupertypeWith<T, U>;
template <class T, class U>
concept __CommonSupertypeWith =
std::same_as<
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>,
std::common_reference_t<
const std::remove_cvref_t<U>&,
const std::remove_cvref_t<T>&>> &&
(std::convertible_to<const std::remove_cvref_t<T>&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>> ||
std::convertible_to<std::remove_cvref_t<T>&&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>>) &&
(std::convertible_to<const std::remove_cvref_t<U>&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>> ||
std::convertible_to<std::remove_cvref_t<U>&&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>>);
In particular, the change is changing common_reference_with to this hypothetical __CommonSupertypeWith where __CommonSupertypeWith differs by allowing for std::common_reference_t<T, U> to produce a reference-stripped version of T or U and also by trying both C(T&&) and C(const T&) to create the common reference. For more details, see P2404.
How do I work around std::equality_comparable_with before this gets merged into the standard?
Change which overload you use
For all of the uses of std::equality_comparable_with (or any of the other *_with concepts) in the standard library, there is helpfully a predicate overload which you can pass a function to. That means that you can just pass std::equal_to() to the predicate overload and get the desired behavior (not std::ranges::equal_to, which is constrained, but the unconstrained std::equal_to).
This doesn't mean that it would be a good idea to not fix std::equality_comparable_with, however.
Can I extend my own types to meet std::equality_comparable_with?
The common-reference requirements use std::common_reference_t, which has a customization point of std::basic_common_reference, for the purpose of:
The class template basic_common_reference is a customization point that allows users to influence the result of common_reference for user-defined types (typically proxy references).
It is a horrible hack, but if we write a proxy reference that supports both types we want to compare, we can specialize std::basic_common_reference for our types, enabling our types to meet std::equality_comparable_with. See also How can I tell the compiler that MyCustomType is equality_comparable_with SomeOtherType? . If you choose to do this, beware; std::common_reference_t is not only used by std::equality_comparable_with or the other comparison_relation_with concepts, you risk causing cascading problems down the road. It is best if you ensure that the common reference is actually a common reference, e.g.:
template <typename T>
class custom_vector { ... };
template <typename T>
class custom_vector_ref { ... };
custom_vector_ref<T> could be a good option for a common reference between custom_vector<T> and custom_vector_ref<T>, or possibly even between custom_vector<T> and std::array<T, N>. Tread carefully.
How can I extend types I don't control std::equality_comparable_with?
You can't. Specializing std::basic_common_reference for types you don't own (either std:: types or some third-party library) is at best bad practice and at worst undefined behavior. The safest choice would be to use a proxy type you own that you can compare through or else write your own extension of std::equality_comparable_with that has an explicit customization point for your custom spelling of equality.
Okay, I get that the idea of these requirements is mathematical soundness, but how do these requirements acheive mathematical soundness, and why is it so important?
Mathematically, equality is an equivalence relation. However, equivalence relations are defined over a single set. So how can we define an equivalence relation between two sets A and B? Simply put, we instead define the equivalence relation over C = A∪B. That is to say, we take a common supertype of A and B and define the equivalence relation over this supertype.
This means that our relation c1 == c2 must be defined no matter where c1 and c2 come from, so we must have a1 == a2, a == b, and b1 == b2 (where ai is from A and bi is from B). Translating to C++, this means that all of operator==(A, A), operator==(A, B), operator==(B, B), and operator==(C, C) must be part of the same equality.
This is why iterator/sentinels do not meet std::equality_comparable_with: while operator==(iterator, sentinel) may actually be part of some equivalence relation, it is not part of the same equivalence relation as operator==(iterator, iterator) (otherwise iterator equality would only answer the question of "Are either both iterators at the end or both iterators not at the end?").
It is actually quite easy to write an operator== that is not actually equality, because you must remember that the heterogeneous equality is not the single operator==(A, B) you are writing, but is instead four different operator==s that must all be coheisve.
Wait a minute, why do we need all four operator==s; why can't we just have operator==(C, C) and operator==(A, B) for optimization purposes?
This is a valid model, and we could do this. However, C++ is not a platonic reality. Although concepts try their hardest to only accept types that truly meet the semantic requirements, it cannot actually acheive this goal. As such, if we were to only check operator==(A, B) and operator==(C, C), we run the risk that operator==(A, A) and operator==(B, B) do something different. Besides, if we can have operator==(C, C), then this means that it is trivial to write operator==(A, A) and operator==(B, B) based on what we have in operator==(C, C). That is to say, the harm of requiring operator==(A, A) and operator==(B, B) is quite low, and in return we get a higher confidence that we actually have an equality.
There are some circumstances where this runs into rough edges, however; see P2405.
How exhausting. Can't we just require that operator==(A, B) is an actual equality? I'm never going to actually use the operator==(A, A) or operator==(B, B) anyway; I only cared about being able to do the cross-type comparison.
Actually, a model where we require operator==(A, B) is an actual equality would probably work. Under this model, we would have std::equality_comparable_with<iterator, sentinel>, but what precisely that means in all known contexts could be hammered out. However, there was a reason why this is not the direction the standard went with, and before one can understand if or how to change it, they must first understand why the standard's model was chosen.
This question cites the C++ standard to demonstrate that the alignment and size of CV qualified types must be the same as the non-CV qualified equivalent type. This seems obvious, because we can implicitly cast an object of type T to a const T& using static_cast or reinterpret_cast.
However, suppose we have two types which both have the same member variable types, except one has all const member variables and the other does not. Such as:
typedef std::pair<T, T> mutable_pair;
typedef std::pair<const T, const T> const_pair;
Here, the standard does not allow us to produce a const_pair& from an instance of mutable_pair. That is, we cannot say:
mutable_pair p;
const_pair& cp = reinterpret_cast<const_pair&>(p);
This would yield undefined behavior, as it is not listed as a valid use of reinterpret_cast in the standard. Yet, there seems to be no reason, conceptually, why this shouldn't be allowed.
So... why should anyone care? You can always just say:
const mutable_pair& cp = p;
Well, you might care in the event you only want ONE member to be const qualified. Such as:
typedef std::pair<T, U> pair;
typedef std::pair<const T, U> const_first_pair;
pair p;
const_first_pair& cp = reinterpret_cast<const_first_pair&>(p);
Obviously that is still undefined behavior. Yet, since CV qualified types must have the same size and alignment, there's no conceptual reason this should be undefined.
So, is there some reason the standard doesn't allow it? Or is it simply a matter that the standard committee didn't think of this use case?
For anyone wondering what sort of use this could have: in my particular case, I ran into a use case where it would have been very useful to be able to cast a std::pair<T, U> to a std::pair<const T, U>&. I was implementing a specialized balanced tree data structure that provides log(N) lookup by key, but internally stores multiple elements per node. The find/insert/rebalance routines requires internal shuffling of data elements. (The data structure is known as a T-tree.) Since internal shuffling of data elements adversely affects performance by triggering countless copy constructors, it is beneficial to implement the internal data shuffling to take advantage of move constructors if possible.
Unfortunately... I also would have liked to be able to provide an interface which meets the C++ standard requirements for AssociativeContainer, which requires a value_type of std::pair<const Key, Data>. Note the const. This means individual pair objects cannot be moved (or at least the keys can't). They have to be copied, because the key is stored as a const object.
To get around this, I would have liked to be able to store elements internally as mutable objects, but simply cast the key to a const reference when the user access them via an iterator. Unfortunately, I can't cast a std::pair<Key, Data> to a std::pair<const Key, Data>&. And I can't provide some kind of workaround that returns a wrapper class or something, because that wouldn't meet the requirements for AssociativeContainer.
Hence this question.
So again, given that the size and alignment requirements of a CV qualified type must be the same as the non-CV qualified equivalent type, is there any conceptual reason why such a cast shouldn't be allowed? Or is it simply something the standard writers didn't really think about?
Having a type as a template parameter does not mean that you won't have different alignments, the class contents could be changed, e.g., via specialization or template metaprogramming. Consider:
template<typename T> struct X { int i; };
template<typename T> struct X<const T> { double i; };
template<typename T> struct Y {
typename std::conditional<std::is_const<T>::value, int, double>::type x;
};
I'm making a simple, non-owning array view class:
template <typename T>
class array_view {
T* data_;
size_t len_;
// ...
};
I want to construct it from any container that has data() and size() member functions, but SFINAE-d correctly such that array_view is only constructible from some container C if it would then be valid and safe behavior to actually traverse data_.
I went with:
template <typename C,
typename D = decltype(std::declval<C>().data()),
typename = std::enable_if_t<
std::is_convertible<D, T*>::value &&
std::is_same<std::remove_cv_t<T>,
std::remove_cv_t<std::remove_pointer_t<D>>>::value>
>
array_view(C&& container)
: data_(container.data()), len_(container.size())
{ }
That seems wholly unsatisfying and I'm not even sure it's correct. Am I correctly including all the right containers and excluding all the wrong ones? Is there an easier way to write this requirement?
If we take a look at the proposed std::experimental::array_view in N4512, we find the following Viewable requirement in Table 104:
Expression Return type Operational semantics
v.size() Convertible to ptrdiff_t
v.data() Type T* such that T* is static_cast(v.data()) points to a
implicitly convertible to U*, contiguous sequence of at least
and is_same_v<remove_cv_t<T>, v.size() objects of (possibly
remove_cv_t<U>> is true. cv-qualified) type remove_cv_t<U>.
That is, the authors are using essentially the same check for .data(), but add another one for .size().
In order to use pointer arithmetic on U by using operations with T, the types need to be similar according to [expr.add]p6. Similarity is defined for qualification conversions, this is why checking for implicit convertibility and then checking similarity (via the is_same) is sufficient for pointer arithmetic.
Of course, there's no guarantee for the operational semantics.
In the Standard Library, the only contiguous containers are std::array and std::vector. There's also std::basic_string which has a .data() member, but std::initializer_list does not, despite it being contiguous.
All of the .data() member functions are specified for each individual class, but they all return an actual pointer (no iterator, no proxy).
This means that checking for the existence of .data() is currently sufficient for Standard Library containers; you'd want to add a check for convertibility to make array_view less greedy (e.g. array_view<int> rejecting some char* data()).
The implementation can of course be moved away from the interface; you could use Concepts, a concepts emulation, or simply enable_if with an appropriate type function. E.g.
template<typename T, typename As,
typename size_rt = decltype(std::declval<T>().size())
typename data_rt = decltype(std::declval<T>().data())>
constexpr bool is_viewable =
std::is_convertible_v<size_rt, std::ptrdiff_t>
&& std::is_convertible_v<data_rt, T*>
&& std::is_same_v<std::remove_cv_t<T>, std::remove_cv_t<data_rt>>;
template <typename C,
typename = std::enable_if_t<is_viewable<C, T>>
>
array_view(C&& container)
: data_(container.data()), len_(container.size())
{ }
And yes, that doesn't follow the usual technique for a type function, but it is shorter and you get the idea.
Given a programmer defined POD struct that will be stored in an unordered_map, is there any particular advantage in defining:
namespace std {
template<>
struct equal_to<MyType> {
bool operator()(const MyType& lhs, const MyType& rhs) const {
...
}
};
}
over simply defining:
operator==(const MyType& lhs, const MyType& rhs)
(I'm already aware of the potential advantage of using an "inlineable" function object rather than a function pointer for the hashing function).
I would say operator== has more uses than a specialization of equal_to<> because people normally write a == b, not equal_to<T>()(a, b). And the default equal_to<> is implemented in terms of operator==, not the other way around.
If you need to specialize std::equal_to because it must behave differently from operator==, then a better idea may be to implement a custom my_equal_to predicate class, not related to std::equal_to in order to follow the principle of least surprise.
Also, there is interface deficiency in std::equal_to<T> because it accepts arguments of the same type. C++14 std::equal_to<void> fixes the deficiency by accepting arguments of different types and forwarding them to operator==.
operator==, on the other hand, can have multiple overloads for different types (e.g. operator==(std::string const&, char const*)).
Which means that in C++14 std::equal_to<void> and overloaded operator== work nicely together, see N3657 Adding heterogeneous comparison lookup to associative containers for more details.
I'm already aware of the potential advantage of using an "inlineable" function object rather than a function pointer for the hashing function
Function pointers do not apply here, default equal_to<> uses operator== directly, not through a pointer.
If you have a class hierarchy and want to use the equal_to of the parent class on two objects of the child class, you may use the equal_to-variant, but not the == variant (which will choose the comperator for the child class):
I know that compilers have much freedom in implementing std::type_info functions' behavior.
I'm thinking about using it to compare object types, so I'd like to be sure that:
std::type_info::name must return two different strings for two different types.
std::type_info::before must say that Type1 is before Type2 exclusive-or Type2 is before Type1.
// like this:
typeid(T1).before( typeid(T2) ) != typeid(T2).before( typeid(T1) )
Two different specialization of the same template class are considered different types.
Two different typedef-initions of the same type are the same type.
And finally:
Since std::type_info is not copyable, how could I store type_infos somewhere (eg: in a std::map)? The only way it to have a std::type_info always allocated somewhere (eg: on the stack or on a static/global variable) and use a pointer to it?
How fast are operator==, operator!= and before on most common compilers? I guess they should only compare a value. And how fast is typeid?
I've got a class A with a virtual bool operator==( const A& ) const. Since A has got many subclasses (some of which are unknown at compile time), I'd overload that virtual operator in any subclass B this way:
virtual bool operator==( const A &other ) const {
if( typeid(*this) != typeid(other) ) return false;
// bool B::operator==( const B &other ) const // is defined for any class B
return operator==( static_cast<B&>( other ) );
}
Is this an acceptable (and standard) way to implement such operator?
After a quick look at the documentation, I would say that :
std::type_info::name always returns two different strings for two different types, otherwise it means that the compiler lost itself while resolving types and you shouldn't use it anymore.
Reference tells : "before returns true if the type precedes the type of rhs in the collation order. The collation order is just an internal order kept by a particular implementation and is not necessarily related to inheritance relations or declaring order."
You therefore have the guarantee that no types has the same rank in the collation order.
Each instantiation of a template class is a different type. Specialization make no exceptions.
I don't really understand what you mean. If you mean something like having typedef foo bar; in two separate compilation units and that bar is the same in both, it works that way. If you mean typedef foo bar; typedef int bar;, it doesn't work (except if foo is int).
About your other questions :
You should store references to std::type_info, of wrap it somehow.
Absolutely no idea about performance, I assume that comparison operators have constant time despite of the type complexity. Before must have linear complexity depending on the number of different types used in your code.
This is really strange imho. You should overload your operator== instead of make it virtual and override it.
Standard 18.5.1 (Class type_info) :
The class type_info describes type
information generated by the
implementation. Objects of this class
effectively store a pointer to a name
for the type, and an encoded value
suitable for comparing two types for
equality or collating order. The
names, encoding rule, and collating
sequence for types are all unspecified
and may differ between programs.
From my understanding :
You don't have this guarantee regarding std:type_info::name. The standard only states that name returns an implementation-defined NTBS, and I believe a conforming implementation could very well return the same string for every type.
I don't know, and the standard isn't clear on this point, so I wouldn't rely on such behavior.
That one should be a definite 'Yes' for me
That one should be a definite 'Yes' for me
Regarding the second set of questions :
No, you cannot store a type_info. Andrei Alexandrescu proposes a TypeInfo wrapper in its Modern C++ Design book. Note that the objects returned by typeid have static storage so you can safely store pointers without worrying about object lifetime
I believe you can assume that type_info comparison are extremely efficient (there really isn't much to compare).
You can store it like this.
class my_type_info
{
public:
my_type_info(const std::type_info& info) : info_(&info){}
std::type_info get() const { return *info_;}
private:
const std::type_info* info_;
};
EDIT:
C++ standard 5.2.8.
The result of a
typeid expression is an lvalue of
static type const std::type_info...
Which means you can use it like this.
my_type_info(typeid(my_type));
The typeid function returns an lvalue (it is not temporary) and therefore the address of the returned type_info is always valid.
The current answers for questions 1 and 2 are perfectly correct, and they're essentially just details for the type_info class - no point in repeating those answers.
For questions 3 and 4, it's important to understand what precisely is a type in C++, and how they relate to names. For starters, there are a whole bunch of predefined types, and those have names: int, float, double. Next, there are some constructed types that do not have names of their own: const int, int*, const int*, int* const. There are function types int (int) and function pointer types int (*)(int).
It's sometimes useful to give a name to an unnamed type, which is possible using typedef. For instance, typedef int* pint or typedef int (*pf)(int);. This introduces a name, not a new type.
Next are user-defined types: structs, classes, unions. There's a good convention to give them names, but it's not mandatory. Don't add such a name with typedef, you can do so directly: struct Foo { }; instead of typedef struct {} Foo;. It's common to have class definitions in headers, which end up\ in multiple translation units. That does mean the class is defined more than once. This is still the same type, and therefore you aren't allowed to play tricks with macros to change the class member definitions.
A template class is not a type, it's a recipe for types. Two instantiations of a single class template are distinct types if the template arguments are different types (or values). This works recursively: Given template <typename T> struct Foo{};, Foo<Foo<int> > is the same type as Foo<Foo<Bar> > if and only if Bar is another name for the type int.
Type_info is implementation defined so I really wouldn't rely on it. However, based on my experiences using g++ and MSVC, assumptions 1,3 and 4 hold... not really sure about #2.
Is there any reason you can't use another method like this?
template<typename T, typename U>
struct is_same { static bool const result = false; };
template<typename T>
struct is_same<T, T> { static bool const result = true; };
template<typename S, typename T>
bool IsSame(const S& s, const T& t) { return is_same<S,T>::result; }
Since std::type_info is not copyable, how could I store type_infos somewhere (eg: in a std::map)? The only way it to have a std::type_info always allocated somewhere (eg: on the stack or on a static/global variable) and use a pointer to it?
This is why std::type_index exists -- it's a wrapper around a type_info & that is copyable and compares (and hashes) by using the underlying type_info operations