Страница 1 из 5
С++ Интересные фишки
Добавлено: Чт, 7 августа 2014, 10:04:59
dyvniy
Просмотр ассемблерного кода компиляторов.
https://godbolt.org/
Добавлено: Пт, 19 сентября 2014, 10:04:27
dyvniy
Простая и эффективная «сборка мусора» (garbage collection) на C++
http://freehabr.ru/blog/cpp/1757.html- Спойлер
- С++
Для незнакомых с этой очень интересной тематикой советую посмотреть обзор на вики. Для более полного понимания проблемы: детальный обзор у Кнута в 1 томе Искусство программирования в разделе 2.5 Динамическое выделение памяти, на хабре, а также одну из лучших реализаций на C/C++ Hans Boehm.
Сразу оговорюсь, предлагаемый мною подход принципиально отличается от вышеизложенных и основан на реализации ООП в C++. В топики приведено описание готовой библиотеки и ссылка на исходники.
Основные положения
Вот цитата из ссылки на вики: Сборщик мусора может нормально функционировать только тогда, когда он может точно отследить все ссылки на все созданные объекты. Очевидно, если язык допускает преобразование ссылок (указателей) в другие типы данных (целые числа, массивы байтов и так далее), такой как Си/Си++, отследить использование таких преобразованных ссылок становится невозможно, и сборка мусора становится бессмысленной — она не защищает от «повисания» ссылок и утечек памяти. Поэтому языки, ориентированные на использование сборки мусора, обычно существенно ограничивают свободу использования указателей, адресной арифметики, преобразований типов указателей к другим типам данных. В части из них вообще нет типа данных «указатель», в части он есть, но не допускает ни преобразований типа, ни изменения.
Я предлагаю следующую альтернативу, ограничить использование указателей внутри класса C++ объявив их в закрытой области видимости. При правильной технологии ООП, а это свойство называется инкапсуляцией, это и так надо делать, пользователь не должен видеть как реализован класс, он должен использовать только его интерфейс. Во-вторых, в C++ имеется выделенный конструктор копирования, который позволяет построить копию объекта. Осталось для выбранного класса определить функцию swap, для подмены объекта на его копию, и можно организовывать «сборку мусора» на C++.
Пример использования
Рассмотрим простейший класс с динамическим выделением памяти, линейный однонаправленный список целых чисел.
01 #include "allocator.h"
02
03 class List {
04 struct Node {
05 int mData;
06 Node* mNext;
07
08 Node(int data, Node* next=NULL):
09 mData(data),
10 mNext(next) {
11 }
12 ~Node() {}
13 };
14
15 Allocator* mAllocator;
16 Node* mHead;
17
Основное отличие от обычного кода строчка 15, где содержится объявление переменной указателя на тип Allocator. В строках 23 и 27 показано его использование для выделения и освобождения памяти. Освобождение использует шаблонную функцию, поскольку перегрузку стандартого delete дополнительными аргументами позволяют не все компиляторы C++. Для базовых типов и классов, для которых в принципе не нужен вызов деструктора, определена шаблонная функция dealloc. Destroy и dealloc могут также дополнительно вызываться с дополнительным целочисленным аргументом для удаления массивов.
18 public:
19 List(Allocator *allocator):
20 …
21 List(const List& a, Allocator *allocator):
22 …
23 *mLink = new(mAllocator) Node(i, *mLink);
24 …
25 Node* tmp=*mLink;
26 *mLink = tmp->mNext;
27 mAllocator->destroy(tmp);
28 …
29 void swap(List &a) {
30 Allocator *tmp1=mAllocator;
31 mAllocator = a.mAllocator;
32 a.mAllocator = tmp1;
33
34 Node* tmp2 = mHead;
35 mHead = a.mHead;
36 a.mHead = tmp2;
37 }
38 …
39 };
В строчках 21 и 29 содержатся заголовки конструктора-копирования и функции swap, которые необходимы для замены текущего объекта на его копию. Если Вы хотите для своего класса использовать оператор =, то его тоже необходимо реализовать. Он, как известно, не наследуется, но поддержка в классе шаблона GC имеется.
Ниже дан пример его использования. На строке 41 объявляем новый тип GCList. Затем вызываем на строке 43 конструктор по умолчанию. Предположим на строке 44 производим массовые операции по выделению/освобождению памяти. Память выделяется блоками, кратными размеру страницы памяти, затем раздается внутри конкретного объекта класса List. Если процент не используемой памяти высок, то вызов reallocate на строке 45 приведет к построению копии объекта, с его последующей заменой на первоначальную переменную a.
40
41 typedef GC<List> GCList;
42 …
43 GCList a;
44 …
45 a.reallocate();
46 …
47
Естественно, вызов reallocate может происходит неоднократно, согласуясь с логикой программы. Также, за счет показанных в следующем разделе очень простых решений попутно выполняются учет работы времени GC, текущая память, максимальная память и в debug версии программы проверки на утечки памяти.
Выделение памяти большими блоками очень выгодно, особенно для большего количества мелких объектов. Стандартные new, delete, malloc, free должны хранить на каждое выделение информацию о начале и конце куска памяти. В данном подходе этого не нужно, поскольку конкретный объект может с помощью конструктора-копирования построить свою копию и обратно очистить используемую память.
Замечу, что предлагаемый подход, позволяет собирать объект из «кирпичиков» с общим Allocator-ом. Лишь бы в каждом инкапсулированом объекте память выделялась/освобождалась с помощью указателя на общий Allocator и были определены, соответствующие конструкторы-копирования и функция swap.
Можно также, как показано ниже, определять Allocator вначале блока и затем его использовать для работы с автоматическими переменными, использующими Allocator. После закрытия блока вся занятая ими память будет возвращена в систему.
{
Allocator a[1];
…
List lst(a);
…
}
Одной из причин, побудивших меня написать такую библиотеку, было заманчивое предложение использовать в качестве менеджера памяти следующей реализации Google. Мои первые тесты сразу показали ускорение на 10-20%, но сразу пришло и разочарование. При многих плюсах самой библиотеки, оказалось, что ускорение достигается за счет вынесения выделения памяти в отдельные потоки и если все процессоры были заняты, то ускорения практически не было. Предлагаемый подход лишен этого недостатка. Память в нем выделяется блоками, а затем уже раздается внутри объекта, поэтому при распараллеливании программы резко снижается количество обращений к глобальной памяти и его менеджеру. Т.е. уменьшается количество переключений между потоками.
Здесь приведены времена работы GC на разных примерах для задачи построения базисов Грёбнера булевых полиномов, которые решают SAT — задачи. Например, использование стандартных malloc и free замедлило, по сравнению с предлагаемым подходом, время расчета примера ezfact32_4.shuffled.cnf c 354.57 сек («сборка мусора» +1.31 сек) до 669.32 сек. В проекте много динамических структур данных и все они используют эту реализацию GC: мономы (в виде массивов номеров переменных разной длины), всевозможные списки, красно-черные деревья, деревья Janet и ZDD-диаграммы.
Ссылка на исходники. Следующий раздел описывает, если интересно, подробности реализации.
Технические детали
В приведенных ниже листингах приведены наглядные комментарии и я думаю не трудно понять, при чтении, основные элементы реализации. Для простоты в коде удаленно использование таймера и код ответственный за переключение на стандартные malloc, free.
01 #include <cstdlib>
02 #include <cassert>
03
04 class Allocator {
05 static size_t sCurrMemory; // Текущая память
06 static size_t sMaxMemory; // Максимальная память
07
08 struct Node {
09 void* mPointer; // Указатель на блок памяти
10 Node* mNext; // Следующий блок памяти
11
12 Node(size_t size, Node* next=NULL):
13 mPointer(malloc(size)),
14 mNext(next) {
15 }
16 ~Node() { free(mPointer); }
17 };
18
19 size_t mAlloc; // Выделенная память
20 size_t mSize; // Используемая память
21 Node* mRoot; // Список блоков памяти
22 size_t mNodeAlloc; // Выделенная память для последнего блока
23 size_t mNodeSize; // Используемая память для последнего блока
24
25 public:
26 static size_t maxMemory() { return sMaxMemory; }
27 static size_t currMemory() { return sCurrMemory; }
28
29 Allocator();
30 Allocator(const Allocator& a);
31 ~Allocator();
32
33 void* allocate(size_t n); // низкоуровневое выделение памяти
34 void deallocate(void* ptr, size_t n); // низкоуровневое освобождение памяти
35
36 template <typename T>
37 inline void dealloc(T *ptr) { // освобождение памяти
38 deallocate(ptr, sizeof(T));
39 }
40 template <typename T>
41 inline void dealloc(T *ptr, size_t n) { // освобождение памяти для массива
42 assert(n > 0);
43 deallocate(ptr, sizeof(T)*n);
44 }
45 template <typename T>
46 inline void destroy(T *ptr) { // освобождение памяти с вызовом деструктора
47 ptr->~T();
48 deallocate(ptr, sizeof(T));
49 }
50 template <typename T>
51 inline void destroy(T *ptr, size_t n) { // освобождение памяти с вызовом деструктора для массива
52 assert(n > 0);
53 for(int i=0; i < n; i++)
54 ptr[i].~T();
55 deallocate(ptr, sizeof(T)*n);
56 }
57
58 size_t alloc() const { return mAlloc; }
59 size_t size() const { return mSize; }
60 bool isGC() const; // возвращает true если нужно делать "сборку мусора"
61 };
62
Вспомогательный класс на строке 63, нужен только потому, что вызов конструктора наследуемого класса происходит раньше, чем для полей конструируемого класса, а деструкторов в обратном порядке. Другими словами конструктор AllocatorPtr будет вызван первым при создании класса GC, а его деструктор соответственно последним.
Кстати, этот пример показывает для чего нужно множественное наследование. А так пришлось бы вводить промежуточный класс, тем самым излишне усложняя код.
63 class AllocatorPtr { // Вспомогательный класс
64 Allocator* mAllocator;
65
66 public:
67 AllocatorPtr():
68 mAllocator(new Allocator) {
69 }
70 AllocatorPtr(const AllocatorPtr& a):
71 mAllocator(new Allocator(*a.mAllocator)) {
72 }
73 ~AllocatorPtr() { delete mAllocator; }
74
75 const Allocator* allocator() const { return mAllocator; }
76 Allocator* allocator() { return mAllocator; }
77
78 void swap(AllocatorPtr& a) {
79 Allocator *tmp=mAllocator;
80 mAllocator = a.mAllocator;
81 a.mAllocator = tmp;
82 }
83 };
84
85 template <typename T>
86 class GC: protected AllocatorPtr, public T {
87 public:
88 GC():
89 AllocatorPtr(),
90 T(allocator()) {
91 }
92 GC(const GC &a):
93 AllocatorPtr(a),
94 T(a, allocator()) {
95 }
96 GC(const T &a):
97 AllocatorPtr(),
98 T(a, allocator()) {
99 }
100 ~GC() {}
101
102 void operator=(const GC &a) {
103 assert(this != &a);
104 T::operator=(a);
105 }
106 void swap(GC &a) {
107 AllocatorPtr::swap(a);
108 T::swap(a);
109 }
110 void reallocate() {
111 if (allocator()->isGC()) {
112 GC a(*this);
113 swap(a);
114 }
115 }
116
117 size_t alloc() const { return allocator()->alloc(); }
118 size_t size() const { return allocator()->size(); }
119 bool isGC() const { return allocator()->isGC(); }
120 };
121
122 using namespace std;
123
124 inline void* operator new(size_t size, GInv::Allocator* allocator) {
125 return allocator->allocate(size);
126 }
127
128 inline void* operator new[](size_t size, GInv::Allocator* allocator) {
129 return allocator->allocate(size);
130 }
131
Здесь реализация в файле allocator.cpp
01 #include "allocator.h"
02
03 const size_t memoryPageSize=4096; // размер страницы
04
05 Allocator::Allocator():
06 mAlloc(0),
07 mSize(0),
08 mRoot(NULL),
09 mNodeAlloc(0),
10 mNodeSize(0) {
11 }
12
13 Allocator::Allocator(const Allocator& a):
14 mAlloc(0),
15 mSize(0),
16 mRoot(NULL),
17 mNodeAlloc(0),
18 mNodeSize(0) {
19 mNodeAlloc = memoryPageSize;
20 mAlloc = mNodeAlloc;
21 mRoot = new Node(mNodeAlloc);
22 sCurrMemory += mNodeAlloc;
23 sMaxMemory = (sMaxMemory > sCurrMemory) ? sMaxMemory: sCurrMemory;
24 }
25
26 Allocator::~Allocator() {
27 assert(mSize == 0);
28 sCurrMemory -= mAlloc;
29 while(mRoot) { // очистка списка блоков памяти
30 Node *tmp = mRoot;
31 mRoot = mRoot->mNext;
32 delete tmp;
33 }
34 }
35
36 void* Allocator::allocate(size_t n) {
37 assert(n > 0);
38 if (mNodeSize + n > mNodeAlloc) { // если памяти в текущем блоке мало
39 mNodeAlloc = ((n + memoryPageSize) / memoryPageSize)*memoryPageSize;
40 mAlloc += mNodeAlloc;
41 mRoot = new Node(mNodeAlloc, mRoot);
42 mNodeSize = 0;
43 sCurrMemory += mNodeAlloc;
44 sMaxMemory = (sMaxMemory >= sCurrMemory) ? sMaxMemory: sCurrMemory;
45 }
46 assert(mNodeSize + n <= mNodeAlloc);
47 void *r=(char*)mRoot->mPointer + mNodeSize;
48 mSize += n;
49 mNodeSize += n;
50 return r;
51 }
52
53 void Allocator::deallocate(void*, size_t n) {
54 assert(n > 0);
55 assert(mSize >= n);
56 mSize -= n;
57 }
58
59 bool Allocator::isGC() const { // чисто эмпирические соображения
60 return mAlloc > memoryPageSize && mAlloc > mSize*2;
61 }
Переопределение new и delete и глюки при вызове из .NET
http://habrahabr.ru/post/99142/- Спойлер
- Проблема глобального переопределения new/delete в C++/CLI
C++*
Как известно, C++ позволяет глобально переопределять операторы new и delete. Обычно такое переопределение используется для диагностики, поиска утечек памяти и более эффективного распределения памяти.
Все это мы используем в нашем крупном проекте. Однако у нас есть часть, написанная на C#, которая с помощью C++/CLI взаимодействует с основной частью на C++. И вот тут появились проблемы. У нас получались утечки памяти там, где их быть ну никак не могло.
Все сводилось к тому, что вызывался наш new, но delete был crt’шный.
Чтобы разобраться с проблемой, я создал тестовый solution, где смоделировал ситуацию. Для простоты переопределенные функции выглядят так:
void* __cdecl operator new( size_t size )
{
printf("New\n");
return malloc(size);
}
void __cdecl operator delete( void *p )
{
printf("Delete\n");
free(p);
}
Значит, мы должны получить одинаковое количество New и Delete в выводе, равное количеству пар new/delete.
Вот код, который вызывает new/delete:
//managed.h
namespace Managed
{
public ref class ManagedClass
{
public:
ManagedClass()
{
NativeClass* cl = new NativeClass();
delete cl;
int* a = new int();
delete a;
}
};
}
//native.cpp
NativeClass::NativeClass()
{
int* a = new int();
delete a;
}
Из C# мы создаем ManagedClass так:
static void Main( string[] args )
{
ManagedClass cl = new ManagedClass();
}
Еще есть класс Foo, который лежит в отдельном файле basic.h, причем он никак не используется в этих классах. Просто его определение помещается в Precompiled Header.
Этот “плохой” класс не несет функциональности, однако одно наличие его приводит к очень странному результату. Если комментируешь конструктор копирования класса, то все работает отлично:
#pragma once
class Foo
{
public:
Foo() {}
//Foo( const Foo& ) {}
Foo& operator = ( const Foo& ) { return *this; }
virtual ~Foo() {}
};
Вывод:
New
New
Delete
Delete
New
Delete
3 new – 3 delete. Все хорошо.
Если же конструктор копирования не закомментирован, то не всем нашим new соответствует наш delete:
#pragma once
class Foo
{
public:
Foo() {}
Foo( const Foo& ) {}
Foo& operator = ( const Foo& ) { return *this; }
virtual ~Foo() {}
};
Вывод:
New
New
Delete
New
3 new – 1 delete. Все плохо.
Итог: мы имеем ошибку выбора функции удаления, которая зависит от “положения луны”. Самое время начать гуглить.
В ходе поисков был найден документ, предположительно описывающий нашу проблему:
http://support.microsoft.com/kb/122675.
К сожалению, после детального прочтения документа, выясняется, что наш случай отличается от рассмотренного. Более того, в pure-C++ проекте такого поведения не наблюдается. Проблема в C++/CLI.
Продолжим наши исследования. Если деструктор сделать не виртуальным, то опять все работает, как надо:
#pragma once
class Foo
{
public:
Foo() {}
Foo( const Foo& ) {}
Foo& operator = ( const Foo& ) { return *this; }
~Foo() {}
};
Вывод:
New
New
Delete
Delete
New
Delete
Стоит заметить, что этот класс никак не используется в нашей программе. Просто само его наличие приводит к багу.
Т.к. баг проявляется только в C++/CLI, то попробуем пометить этот класс как unmanaged:
#pragma managed(push,off)
#include "../System/basic.h" // h файл с “плохим” классом Foo
#pragma managed(pop)
Вывод:
New
New
Delete
Delete
New
Delete
Получилось! Однако, это не решение проблемы. Будем копать дальше.
Может быть проблема в Precompiled Headers? Однако если basic.h из Stdafx.h, но вставить его в любой другой *.h файл, который попадает в проект, то проблема повторится.
Посмотрим более подробно, что делает linker. Для этого включим режим вывода дополнительной информации у linker’а:
image
Ого! Он нашел delete в MSVCRTD.lib, вот почему delete используется не наш:
image
В случае же, когда мы делаем деструктор не виртуальным или комментируем конструктор копирования, получаем такой выход linker’а:
image
Более того, если оставить конструктор копирования, сделать деструктор не виртуальным, но добавить виртуальную функцию, опять delete начинает глючить.
Хотя исследования были в Debug сборке, но и в Release наблюдается то же самое поведение.
К сожалению, данную проблему пока не удалось решить, что приводит к невозможности простого поиска утечек памяти в проекте.
В итоге имеем, что при наличии класса, у которого есть виртуальная таблица и конструктор копирования (причем этот класс компилируется в managed), linker линкует стандартный delete, хотя у нас и есть переопределенный delete в dll.
PS. Кому интересно самостоятельно поработать с простейшим тестовым solution с проблемой и, возможно, помочь советом, могут скачать его вот здесь: https://mysvn.ru/Lof/NewDeleteProblem
Добавлено: Пн, 22 сентября 2014, 12:50:35
dyvniy
Лямбды
без захвата конвертируются в обычную функцию
Давай поиграем в игру!
http://habrahabr.ru/company/pvs-studio/blog/237219/
сама игра
http://q.viva64.com/Статическая линковка вижуал студии.
http://stackoverflow.com/questions/3007312/resolv ... faultlib-msvcrt-conflicts-withКод: Выделить всё
There are 4 versions of the CRT link libraries present in vc\lib:
libcmt.lib: static CRT link library for a release build (/MT)
libcmtd.lib: static CRT link library for a debug build (/MTd)
msvcrt.lib: import library for the release DLL version of the CRT (/MD)
msvcrtd.lib: import library for the debug DLL version of the CRT (/MDd)
косяк с двумя менеджерами памяти при статической линковке
http://www.gamedev.ru/code/forum/?id=126869http://rsdn.ru/forum/cpp/3505085.flatчтоб решить эту проблему, надо создать функцию удаления в том же модуле и экспортировать её.
Код: Выделить всё
PANCAR_API void Deleter(CTestObject* p)
{
delete p;
}
Добавлено: Вт, 21 октября 2014, 14:43:38
dyvniy
Логирование использование памяти.
Код: Выделить всё
#define gdpnew(x) new(__FILE__, __LINE__, typeid(x).name() ) x
Добавлено: Вт, 27 января 2015, 15:55:51
dyvniy
Option<type> = nullable from C#
Код: Выделить всё
template<class T>
class OptionBase
{
public:
OptionBase() :
data_(nullptr)
{
}
protected:
bool filled_() const
{
return data_ != nullptr;
}
void free_()
{
if ( data_ != nullptr )
{
data_->~T();
data_ = nullptr;
}
}
void set_(T&& t)
{
data_ = new (raw_()) T(std::forward<T>(t));
}
void set_(const T& t)
{
data_ = new (raw_()) T(t);
}
T* pointer_()
{
return data_;
}
const T* pointer_() const
{
return data_;
}
private:
void* raw_()
{
return static_cast<void*>(&buffer_);
}
const void* raw_() const
{
return static_cast<const void*>(&buffer_);
}
T* data_;
typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type buffer_;
};
template<class T>
class Option : public OptionBase<T>
{
public:
Option(typename std::remove_reference<T>::type&& t)
{
set_(std::forward<T>(t));
}
Option(const T& t)
{
set_(t);
}
Option(nullptr_t)
{
}
Option()
{}
~Option()
{
free_();
}
Option(const Option& from)
{
if ( from )
{
set_(*from);
}
}
Option(Option&& from)
{
if ( from )
{
set_(std::move(*from));
from.free_();
}
}
Option& operator=(T&& t)
{
if ( *this )
{
(**this) = std::forward<T>(t);
}
else
{
set_(std::forward<T>(t));
}
return *this;
}
Option& operator=(const Option& other)
{
if ( !other )
{
free_();
return *this;
}
if ( *this )
{
(**this) = *other;
}
else
{
set_(*other);
}
return *this;
}
Option& operator=(Option&& other)
{
if ( !other )
{
free_();
return *this;
}
if ( *this )
{
(**this) = std::move(*other);
}
else
{
set_(std::move(*other));
}
other.free_();
return *this;
}
Option& operator=(nullptr_t)
{
free_();
return *this;
}
bool operator==(const Option& other) const
{
if ( *this && other )
{
return **this == *other;
}
if ( !*this && !other )
{
return true;
}
return false;
}
bool operator==(const T& t) const
{
if ( *this )
{
return **this == t;
}
return false;
}
bool operator!=(const Option& other) const
{
return !(*this == other);
}
typename std::remove_reference<T>::type& operator*()
{
Assert_basic(*this);
return *pointer_();
}
typename std::remove_reference<T>::type* operator->()
{
Assert_basic(*this);
return pointer_();
}
typename std::remove_reference<T>::type const& operator*() const
{
Assert_basic(*this);
return *pointer_();
}
typename std::remove_reference<T>::type const* operator->() const
{
Assert_basic(*this);
return pointer_();
}
private:
typedef void (Option::*Bool) () const;
void boolTestable() const {}
public:
operator Bool () const
{
return filled_()? &Option::boolTestable : nullptr;
}
};
template<class T>
bool operator==(const Option<T>& o, const T& t)
{
if ( o )
{
return *o == t;
}
return false;
}
template<class T>
bool operator!=(const Option<T>& o, const T& t)
{
return !(o == t);
}
template<class T>
bool operator==(const T& t, const Option<T>& o)
{
return o == t;
}
template<class T>
bool operator!=(const T& t, const Option<T>& o)
{
return o != t;
}
template<class T>
class OptionBase<T&>
{
public:
OptionBase(): t_(nullptr)
{
}
protected:
bool filled_() const
{
return t_ != nullptr;
}
void free_()
{
t_ = nullptr;
}
void set_(T& t)
{
t_ = &t;
}
T* pointer_()
{
return t_;
}
const T* pointer_() const
{
return t_;
}
private:
T* t_;
};
Добавлено: Вс, 7 июня 2015, 13:41:40
dyvniy
ipp что за файл
http://julipedia.meroh.net/2013/12/header-files-c-ipp-files.html- Спойлер
- Header files: C++ ipp files
Hoping you had a nice holiday break, it is now the time to resume our series on header files with a new topic covering the world of template definitions in C++.
If you have ever used the Boost libraries, you may have noticed that aside from the regular hpp header files, they also provide a bunch of accompanying ipp files.
ipp files, in the general case, are used to provide the implementation for a template class defined in a corresponding hpp file. This stems from the fact that, in C++, the code for such template definitions must be available whenever the class is instantiated, and this in turn means that the template definitions cannot be placed in separate modules as is customary with non-template classes. In other words: putting the template definitions in cpp files just does not work. (I hear the C++ standards committee wants to "fix" this but I forget the details now and cannot find a reference.)
By splitting the implementation of a template class from its definition, the developer controls when and where the implementation of such class is instantiated and made available. The developer does this by explicitly including the ipp file instead of the hpp file where necessary.
There are two advantages to doing the above:
Shorter build times: Most modules where you include a header file wouldn't care less about the implementation internals of the class they need. If you don't need something in a module, don't include it in the first place!
Cleaner header files: Because classes exist to encapsulate some behavior, it is extremely likely for the implementation of a class to require the inclusion of more header files than its corresponding hpp counterpart. By keeping the two separate, the hpp files are leaner dependency-wise and prevent inadvertently introducing additional indirect dependencies in your code. This, again, is just a side-effect of C++ lacking a proper modules system.
Obviously, all of the above applies not only to template class definitions, but also to template functions and even inline functions. The guideline, therefore, is: place definitions in cpp or ipp files and restrict hpp files to contain declarations only.
And keep in mind that including ipp files from within hpp files is almost always the wrong thing to do.