shared_ptr обычно реализуется через подсчет ссылок - количества объектов, которые ссылаются на указатель. При копировании shared_ptr количество увеличивается на 1, деструктор уменьшает количество на 1. При достижении числа 0 вызывается deleter (по-умолчанию delete).
Для иллюстрации воспользуемся методом use_count():
Как видим, weak_ptr создается из shared_ptr, но не увеличивает число shared_ptr, указывающих на этот объект. На самом деле, для weak_ptr имеется отдельный счетчик ссылок, он изменяется при создании "слабой" ссылки.
Для получения указателя из weak_ptr необходимо вызвать метод lock(), который вернет соответствующий shared_ptr, что увеличит число ссылок. Другого способа аналогичного методу get() для получения значения указателя из weak_ptr нет.
weak_ptr может спокойно пережить свой shared_ptr и это безопасно с ним работать после удаления соотв. указателя, но в таком случае lock() вернет указатель на nullptr.
Для иллюстрации воспользуемся методом use_count():
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> shared = std::make_shared<int>(100);
std::cout << shared.use_count() << std::endl; // 1
{
auto shared2 = shared;
std::cout << shared.use_count() << std::endl; // 2
}
std::cout << shared.use_count() << std::endl; // 1
std::weak_ptr<int> weak = shared;
std::cout << shared.use_count() << std::endl; // 1
if (auto shared3 = weak.lock()) { // получить shared_ptr из weak_ptr
std::cout << shared.use_count() << std::endl; // 2
}
std::cout << shared.use_count() << std::endl; // 1
shared.reset(); // чистим
std::cout << shared.use_count() << std::endl; // 0
auto shared4 = weak.lock();
if (shared4 == nullptr) {
std::cout << "can't lock";
}
return 0;
}
// Output:
// 1
// 2
// 1
// 1
// 2
// 1
// 0
// can't lock
Как видим, weak_ptr создается из shared_ptr, но не увеличивает число shared_ptr, указывающих на этот объект. На самом деле, для weak_ptr имеется отдельный счетчик ссылок, он изменяется при создании "слабой" ссылки.
Для получения указателя из weak_ptr необходимо вызвать метод lock(), который вернет соответствующий shared_ptr, что увеличит число ссылок. Другого способа аналогичного методу get() для получения значения указателя из weak_ptr нет.
weak_ptr может спокойно пережить свой shared_ptr и это безопасно с ним работать после удаления соотв. указателя, но в таком случае lock() вернет указатель на nullptr.
❤1👍1🔥1🏆1
Давайте углубимся в детали реализации shared_ptr и может быть немного повторимся.
В типичной реализации, shared_ptr содержит 2 указателя:
- собственно хранимый указатель (тот, что вернет метод get())
- указатель на управляющий блок (control block)
Управляющий блок - это динамически выделяемый объект, который содержит:
- или указатель на управляемый объект (да да, еще один указатель) или сам управляемый объект (подробности дальше)
- deleter
- аллокатор
- число shared_ptr-ов, которые владеют управляемым объектом
- число weak_ptr ("слабый" счетчик), которые владеют управляемым объектом
Когда shared_ptr создается с помощью std::make_shared(), то память под управляющий блок и сам объект создается одним выделением. Управляемый объект создается "на месте" в самом управляющем блоке. Если же создавать shared_ptr через конструктор, то управляемый блок и управляемый объект выделяются отдельно и управляющий блок хранит указатель на объект.
Кстати, вот что пишет Скотт Мэйерс про счетчик ссылок weak_ptr: "На практике значение слабого счетчика не всегда совпадает с количеством указателей std::weak_ptr, ссылающихся на управляющий блок, поскольку разработчики библиотеки нашли способы добавлять в слабый счетчик дополнительную информацию, которая упрощает генерацию лучшего кода". И да, для проверки просроченности указателя, weak_ptr пользуется "обычным" счетчиком ссылок, а не "слабым".
А вот сейчас жесть: указатель, хранимый shared_ptr непосредственно - это тот, который вернется методом get(), тогда как указатель, хранимый управляющим блоком, этот тот, который будет удален когда число шарных указателей достигнет нуля. Эти указатели необязательно равны (хотя с такой ситуацией вы вряд ли столкнетесь)! Если вы ничего не поняли, то это ок , но детали тут.
Деструктор шарного указателя уменьшает число шарных владельцев в контрольном блоке. Если это число падает до нуля, то контрольный блок вызывает деструктор управляемого объекта. Контрольный блок не удаляет сам себя до тех пор. пока и число weak_ptr не упадет до нуля (если weak_ptr не было, то их число сразу 0 и контрольный блок уничтожится).
В типичной реализации, shared_ptr содержит 2 указателя:
- собственно хранимый указатель (тот, что вернет метод get())
- указатель на управляющий блок (control block)
Управляющий блок - это динамически выделяемый объект, который содержит:
- или указатель на управляемый объект (да да, еще один указатель) или сам управляемый объект (подробности дальше)
- deleter
- аллокатор
- число shared_ptr-ов, которые владеют управляемым объектом
- число weak_ptr ("слабый" счетчик), которые владеют управляемым объектом
Когда shared_ptr создается с помощью std::make_shared(), то память под управляющий блок и сам объект создается одним выделением. Управляемый объект создается "на месте" в самом управляющем блоке. Если же создавать shared_ptr через конструктор, то управляемый блок и управляемый объект выделяются отдельно и управляющий блок хранит указатель на объект.
Кстати, вот что пишет Скотт Мэйерс про счетчик ссылок weak_ptr: "На практике значение слабого счетчика не всегда совпадает с количеством указателей std::weak_ptr, ссылающихся на управляющий блок, поскольку разработчики библиотеки нашли способы добавлять в слабый счетчик дополнительную информацию, которая упрощает генерацию лучшего кода". И да, для проверки просроченности указателя, weak_ptr пользуется "обычным" счетчиком ссылок, а не "слабым".
А вот сейчас жесть: указатель, хранимый shared_ptr непосредственно - это тот, который вернется методом get(), тогда как указатель, хранимый управляющим блоком, этот тот, который будет удален когда число шарных указателей достигнет нуля. Эти указатели необязательно равны (хотя с такой ситуацией вы вряд ли столкнетесь)! Если вы ничего не поняли, то это ок , но детали тут.
Деструктор шарного указателя уменьшает число шарных владельцев в контрольном блоке. Если это число падает до нуля, то контрольный блок вызывает деструктор управляемого объекта. Контрольный блок не удаляет сам себя до тех пор. пока и число weak_ptr не упадет до нуля (если weak_ptr не было, то их число сразу 0 и контрольный блок уничтожится).
🔥2❤1👎1
В прошлый раз я писала, что make_shared разместит объект прямо в управляющем блоке, а также что управляющий блок не разрушится, если уничтожены все shared_ptr, но есть хотя бы один weak_ptr, отслеживающий этот контрольный блок. Это значит, что после разрушения последнего шарного указателя память может значительное время не освобождаться (повторюсь, при использовании make_shared).
Давайте проведем небольшой тест: создадим строку размером 100MB с помощью make_shared, затем получим weak_ptr ссылку, сбросим shared_ptr и зациклимся с "живым" weak_ptr:
Что же у меня получилось? Изначально память процесса действительно занимает около 100 МБ, а через несколько секунд (когда shared_ptr сбрасывается) резко падает до < 1 МБ. То есть память все-таки освободилась в момент разрушения последнего shared_ptr.
В чем дело? Да ХЗ, видимо кривой примерчик или компилятор нужно как-то хитрее обмануть. Какие ваши идеи?
Давайте проведем небольшой тест: создадим строку размером 100MB с помощью make_shared, затем получим weak_ptr ссылку, сбросим shared_ptr и зациклимся с "живым" weak_ptr:
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
#define MAKE_SHARED 1
constexpr size_t strSize = 100 * 1024 * 1024; // 100 MB
int main()
{
#ifdef MAKE_SHARED
std::shared_ptr<std::string> shared
= std::make_shared<std::string>(strSize, 'z');
#else
std::shared_ptr<std::string> shared;
shared.reset(new std::string(strSize, 'z'));
#endif
std::weak_ptr<std::string> weak(shared);
for (;;) {
std::this_thread::sleep_for(500ms);
if (shared) {
std::cout << (*shared)[0];
}
if (std::rand() % 10 == 0) { // try to fool the compiler
shared.reset();
}
if (std::rand() == -3) { // never happens, but
break; // compiler is stupid
}
}
std::cout << std::endl;
if (auto shared2 = weak.lock()) {
std::cout << (*shared2)[0];
}
return 0;
}
Что же у меня получилось? Изначально память процесса действительно занимает около 100 МБ, а через несколько секунд (когда shared_ptr сбрасывается) резко падает до < 1 МБ. То есть память все-таки освободилась в момент разрушения последнего shared_ptr.
В чем дело? Да ХЗ, видимо кривой примерчик или компилятор нужно как-то хитрее обмануть. Какие ваши идеи?
❤4
Type traits часть 1 (перевод)
Type traits - это фича C++, которая позволяет выполнить compile-time анализ типов в нашем коде.
Использование type traits
Стандартная библиотека C++ включает большую коллекцию type traits и связанных функций в заголовке <type_traits>:
Почти все type traits стандартной библиотеки являются шаблонизированными структурами. Они принимают тип для анализа как параметр шаблона и предоставляют результат анализа через статическое поле value.
Например, std::is_arithmetic сообщает, является ли тип числовым:
Мы можем использовать эти булевы значения как обычно в нашем коде:
Начиная с C++17, есть сокращенный способ получения
Более полезным случаем будет использование трейтов в ситуации, когда мы не знаем типа, то есть с шаблонами:
Type traits - это фича C++, которая позволяет выполнить compile-time анализ типов в нашем коде.
Использование type traits
Стандартная библиотека C++ включает большую коллекцию type traits и связанных функций в заголовке <type_traits>:
#include <type_traits>
Почти все type traits стандартной библиотеки являются шаблонизированными структурами. Они принимают тип для анализа как параметр шаблона и предоставляют результат анализа через статическое поле value.
Например, std::is_arithmetic сообщает, является ли тип числовым:
std::is_arithmetic<int>::value // true
std::is_arithmetic<std::string>::value // false
Мы можем использовать эти булевы значения как обычно в нашем коде:
#include <type_traits>
#include <iostream>
#include <vector>
int main() {
if (std::is_arithmetic<float>::value) {
std::cout << "float is arithmetic";
}
if (!std::is_arithmetic<std::string>::value) {
std::cout << " but std::string isn't";
}
using vector_trait =
std::is_arithmetic<std::vector<int>>;
if (!vector_trait::value) {
std::cout
<< "\nNeither is std::vector<int>";
}
}
// Output:
// float is arithmetic but std::string isn't
// Neither is std::vector<int>
Начиная с C++17, есть сокращенный способ получения
value: добавить _v к имени класса:#include <type_traits>
#include <iostream>
int main() {
if (std::is_arithmetic_v<double>) {
std::cout << "double is arithmetic\n";
}
return 0;
}
Более полезным случаем будет использование трейтов в ситуации, когда мы не знаем типа, то есть с шаблонами:
#include <type_traits>
#include <iostream>
template <typename T>
void Function(T Param) {
if (std::is_arithmetic<T>::value) {
std::cout << "Type is arithmetic\n";
} else {
std::cout << "Type is not arithmetic\n";
}
}
int main() {
Function(100);
Function("Hello World");
return 0;
}
// Output:
// Type is arithmetic
// Type is not arithmetic
❤3👍1🔥1
Type traits часть 2 (перевод)
Compile-type условия с if constexpr
Напоминаем, что type trait вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить
Проверка if во время компиляции позволяет полностью удалить весь неподходящий код. Можно представить, что остается такой код:
Рассмотрим еще пример с другими трейтами:
Больше трейтов описано тут.
Compile-type условия с if constexpr
Напоминаем, что type trait вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить
constexpr к базовому if (нужен C++17):#include <type_traits>
#include <iostream>
template <typename T>
void Function(T Param) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer!\n";
}
if constexpr (std::is_floating_point_v<T>) {
std::cout << "Floating point number!\n";
}
}
int main() {
Function(42);
Function(3.14);
return 0;
}
// Output:
// Integer!
// Floating point number!
Проверка if во время компиляции позволяет полностью удалить весь неподходящий код. Можно представить, что остается такой код:
void Function(int Param) {
std::cout << "Integer!\n";
}
void Function(double Param) {
std::cout << "Floating point number!\n";
}Рассмотрим еще пример с другими трейтами:
#include <type_traits>
#include <iostream>
class Actor {};
class Monster : Actor {};
template <typename T>
void Function(T Param) {
using namespace std;
if constexpr (is_pointer_v<T>) {
cout << "\nType is a pointer";
}
if constexpr (is_class_v<T>) {
cout << "\nType is a class";
}
if constexpr (is_same_v<Actor, T>) {
cout << ": Actor";
} else if constexpr (is_base_of_v<Actor, T>) {
cout << ": Actor (or derived from Actor)";
}
}
int main() {
int x{5};
Function(&x);
Actor MyActor;
Function(MyActor);
Monster MyMonster;
Function(MyMonster);
return 0;
}
// Output:
// Type is a pointer
// Type is a class: Actor
// Type is a class: Actor (or derived from Actor)
Больше трейтов описано тут.
👍4🔥2
Type traits часть 3 (перевод)
Рассмотрим такую простую шаблонную функцию:
Эта функция позволяет передать два числовых значения и получить их среднее. Так как функция шаблонизированная, то мы можем передать любые типы.
Однако такой код позволяет слишком многое. Хотя наша функция предназначения для работы с числами, это явно нигде не обозначено. Что если передвать в нее не числовой тип? Есть два варианта развития событий.
В лучшем случае, используемый тип не будет поддерживать один из операторов, например /. Это приведет к ошибке компиляции, хотя и не очень понятной:
В худжем случае, переданный тип реализует все требуемые операторы, но их поведение не такое же, как предполагает наша функция (а именно арифметическое сложение и деление для + и /). Ведь типы свободны перегружать операторы + и / для любой цели, они не обязаны означать сложение и деление.
В таком случае, компилятор с радостью создаст экземпляр шаблона, так как тип соответвует всем требованиям. И мы не получим ошибку компиляции, хотя получили баг.
Type traits позволяют нам быть более явными относительно требований. Самый базовый пример - это просто использовать static_assert:
Это имеет три преимущества:
- реализация требований. Теперь компилятор будет проверять, что предоставленный тип это число, а не просто что он реализует операторы + и /
- документация требований. Любой, кто взглянет на нашу функцию, увидит какие есть требования. Некоторые IDE смогут сразу предоставить обратную связь до компиляции.
- более полезные сообщения об ошибках, если тип не соответвует требованиям:
Рассмотрим такую простую шаблонную функцию:
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
return (x + y) / 2;
}
Эта функция позволяет передать два числовых значения и получить их среднее. Так как функция шаблонизированная, то мы можем передать любые типы.
Однако такой код позволяет слишком многое. Хотя наша функция предназначения для работы с числами, это явно нигде не обозначено. Что если передвать в нее не числовой тип? Есть два варианта развития событий.
В лучшем случае, используемый тип не будет поддерживать один из операторов, например /. Это приведет к ошибке компиляции, хотя и не очень понятной:
binary '/':
'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
does not define this operator or a conversion to a type acceptable
to the predefined operator
В худжем случае, переданный тип реализует все требуемые операторы, но их поведение не такое же, как предполагает наша функция (а именно арифметическое сложение и деление для + и /). Ведь типы свободны перегружать операторы + и / для любой цели, они не обязаны означать сложение и деление.
В таком случае, компилятор с радостью создаст экземпляр шаблона, так как тип соответвует всем требованиям. И мы не получим ошибку компиляции, хотя получили баг.
Type traits позволяют нам быть более явными относительно требований. Самый базовый пример - это просто использовать static_assert:
#include <type_traits>
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
static_assert(
std::is_arithmetic<TFirst>::value,
"Average: First argument must be arithmetic");
static_assert(
std::is_arithmetic<TSecond>::value,
"Average: Second argument must be arithmetic");
return (x + y) / 2;
}
Это имеет три преимущества:
- реализация требований. Теперь компилятор будет проверять, что предоставленный тип это число, а не просто что он реализует операторы + и /
- документация требований. Любой, кто взглянет на нашу функцию, увидит какие есть требования. Некоторые IDE смогут сразу предоставить обратную связь до компиляции.
- более полезные сообщения об ошибках, если тип не соответвует требованиям:
int main() {
Average("Hello", "World");
}static_assert failed: 'Average: First argument must be arithmetic'
static_assert failed: 'Average: Second argument must be arithmetic'
👍5
Задачка для собеса. Пусть дан вектор беззнаковых целых, в которых есть нули. Нужно сдвинуть все нули в конец вектора, то есть написать код функции, используя стандартные алгоритмы:
Конечно, можно написать код самому, но так вы не покажете знания std. Как вариант, можно сделать сортировку в обратном порядке:
но тогда порядок нарушится и плюс это занимает O(N·log(N)).
Другим более интересным вариантом будет использование std::remove:
Дело в том что remove (в отличие от std::erase) не удаляет физически элементы из контейнера (про это уже было писано на канале). Вместо этого, нужные элементы сдвигаются в начало контейнера, а в конце остается "мусор". Нам остается только забить нулями "мусор", начало которого remove и вернет.
#include <iostream>
#include <vector>
#include <algorithm>
void move_zeroes_to_end(std::vector<size_t>& v)
{
// TODO
}
int main()
{
std::vector<size_t> v = { 0, 4, 8, 3, 7, 0, 1, 4, 4, 0 };
move_zeroes_to_end(v);
for (auto i : v) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
Конечно, можно написать код самому, но так вы не покажете знания std. Как вариант, можно сделать сортировку в обратном порядке:
std::sort(v.rbegin(), v.rend());но тогда порядок нарушится и плюс это занимает O(N·log(N)).
Другим более интересным вариантом будет использование std::remove:
auto zero_it = std::remove(v.begin(), v.end(), 0);
std::fill(zero_it, v.end(), 0);
Дело в том что remove (в отличие от std::erase) не удаляет физически элементы из контейнера (про это уже было писано на канале). Вместо этого, нужные элементы сдвигаются в начало контейнера, а в конце остается "мусор". Нам остается только забить нулями "мусор", начало которого remove и вернет.
👍3❤1
Оператор присвоения копии получает аргумент того же типа, что и класс.
Для совместимости с оператором присвоения встроенных типов операторы присвоения обычно возвращают (неконстантную) ссылку на свой левый операнд:
Как видим, благодаря возврату ссылки, работает множественное присваивание:
Однако, оператор присвоения копии может возвращать любое значение.
Если мы захотим возвращать ничего, то это тоже возможно:
В этом случае, множественное присваивание не сработает, однако единичное вполне:
Мы также можем (например по ошибке) вернуть не ссылку, а сам объект, а также например строку:
Конечно, так делать не рекомендуется.
Кстати, компилятор будет синтезировать оператор присвоения копии с возвратом неконстантной (кто знает почему?) ссылки на левый операнд.
Для совместимости с оператором присвоения встроенных типов операторы присвоения обычно возвращают (неконстантную) ссылку на свой левый операнд:
#include <iostream>
class Data
{
public:
Data(int i = 0) : _i(i) {}
Data& operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return *this;
}
int i() const { return _i; }
private:
int _i;
};
int main() {
Data d1(1);
Data d2(2);
Data d3(3);
d1 = d2 = d3;
std::cout << d1.i() << d2.i() << d3.i() << std::endl;
return 0;
}
// Output:
// operator= 2
// operator= 1
// 333
Как видим, благодаря возврату ссылки, работает множественное присваивание:
d1 = d2 = d3;.Однако, оператор присвоения копии может возвращать любое значение.
Если мы захотим возвращать ничего, то это тоже возможно:
void operator=(const Data & other)В этом случае, множественное присваивание не сработает, однако единичное вполне:
d1 = d2 = d3; // ошибка!
d1 = d2; // ок
Мы также можем (например по ошибке) вернуть не ссылку, а сам объект, а также например строку:
#include <iostream>
#include <string>
class Data
{
public:
Data(int i = 0) : _i(i) {}
std::string operator=(const Data & other) // copy assignment
{
std::cout << "operator= " << _i << std::endl;
_i = other._i;
return "lovite narkomana";
}
int i() const { return _i; }
private:
int _i;
};
int main() {
Data d1(1);
Data d2(2);
std::cout << (d1 = d2) << std::endl;
return 0;
}
// Output:
// operator= 1
// lovite narkomana
Конечно, так делать не рекомендуется.
Кстати, компилятор будет синтезировать оператор присвоения копии с возвратом неконстантной (кто знает почему?) ссылки на левый операнд.
🤔1
Type traits часть 4 (перевод)
std::enable_if и SFINAE
Трейт std::enable_if принимает два параметра - булево значение и тип. Если значение булева параметра является истинным, то результирующая структура (enable_if это параметризированная структура) содержит поле
Представим такой код:
Во время компиляции, можно представить что наш enable_if код выльется просто в
Обычно мы используем алиас std::enable_if_t, который дает прямой доступ к
Давайте посмотрим, что будет если булево значение ложно:
В этом случае можно представить, что код эквивалентен такому:
Этот код невалиден и мы получим ошибку компиляции. И какая от всего этого польза? Она связана с правилом из мира шаблонов, называемого SFINAE (substitution failure is not an error - «неудавшаяся подстановка — не ошибка»). В общем, правило гласит: Если не получается рассчитать окончательные типы шаблонных параметров функции, компилятор не выбрасывает ошибку. Этот шаблон просто игнорируется и компилятор переходит к другим кандидатам.
Мы можем создать шаблонные функции, которые в зависимости от условий могут привести к ошибке подстановки. Например:
Если T это класс, то наша подставка будет выглядеть как-то так:
Код выше вполне себе валидный, так что наша функция будет способна обработать наш вызов. Однако, если T это не класс, то шаблон после подстановки будет выглядеть вот так:
В этот раз получившийся код не является валидным. Но, согласно правилу SFINAE, ошибка подстановки не обязательно значит ошибку компиляции. Ведь может быть другой шаблон, который подойдет.
В примере ниже, у нас есть шаблон, который приведет к ошибке подстановки, если тип является классом и другой, если тип не является классом.
Эта программа успешно скомпилится и выполнится, потому что все вызовы функций были покрыты шаблонами, которые не пострадали от ошибки подстановки.
Если код выше выглядит непонятным - то это так. Минимум - это запомнить, что такое правило есть и понять в чем его суть.
Стоит отметить, что в C++20 для упрощения темы шаблонного программирования были добавлены концепты.
std::enable_if и SFINAE
Трейт std::enable_if принимает два параметра - булево значение и тип. Если значение булева параметра является истинным, то результирующая структура (enable_if это параметризированная структура) содержит поле
type с типом второго параметра.Представим такой код:
#include <type_traits>
int main() {
std::enable_if<true, int>::type MyInt{42};
}
Во время компиляции, можно представить что наш enable_if код выльется просто в
int, что сделает наш код эквивалентным:#include <type_traits>
int main() {
int MyInt{42};
}
Обычно мы используем алиас std::enable_if_t, который дает прямой доступ к
type:std::enable_if_t<true, int> MyInt{42};Давайте посмотрим, что будет если булево значение ложно:
#include <type_traits>
int main() {
std::enable_if<false, int>::type MyInt{42};
}
В этом случае можно представить, что код эквивалентен такому:
#include <type_traits>
int main() {
MyInt{42};
}
Этот код невалиден и мы получим ошибку компиляции. И какая от всего этого польза? Она связана с правилом из мира шаблонов, называемого SFINAE (substitution failure is not an error - «неудавшаяся подстановка — не ошибка»). В общем, правило гласит: Если не получается рассчитать окончательные типы шаблонных параметров функции, компилятор не выбрасывает ошибку. Этот шаблон просто игнорируется и компилятор переходит к другим кандидатам.
Мы можем создать шаблонные функции, которые в зависимости от условий могут привести к ошибке подстановки. Например:
template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {}
Если T это класс, то наша подставка будет выглядеть как-то так:
template <typename T, int = 0>
void Function(T Arg) {}
Код выше вполне себе валидный, так что наша функция будет способна обработать наш вызов. Однако, если T это не класс, то шаблон после подстановки будет выглядеть вот так:
template <typename T, = 0>
void Function(T Arg) {}
В этот раз получившийся код не является валидным. Но, согласно правилу SFINAE, ошибка подстановки не обязательно значит ошибку компиляции. Ведь может быть другой шаблон, который подойдет.
В примере ниже, у нас есть шаблон, который приведет к ошибке подстановки, если тип является классом и другой, если тип не является классом.
#include <type_traits>
#include <iostream>
template <typename T,
std::enable_if_t<std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That was a class\n";
}
template <typename T,
std::enable_if_t<!std::is_class_v<T>, int> = 0>
void Function(T Arg) {
std::cout << "That wasn't\n";
}
class Character {};
int main() {
Function(Character{});
Function(3.14);
}
// Output:
// That was a class
// That wasn't
Эта программа успешно скомпилится и выполнится, потому что все вызовы функций были покрыты шаблонами, которые не пострадали от ошибки подстановки.
Если код выше выглядит непонятным - то это так. Минимум - это запомнить, что такое правило есть и понять в чем его суть.
Стоит отметить, что в C++20 для упрощения темы шаблонного программирования были добавлены концепты.
👍1
Исходники генератора подземелий для вашей визитки 😁.
https://gist.github.com/munificent/b1bcd969063da3e6c298be070a22b604
#юмор
https://gist.github.com/munificent/b1bcd969063da3e6c298be070a22b604
#юмор
❤🔥1😁1
Type traits часть 5 (перевод)
Условный выбор типа с помощью std::conditional
В заголовке
Несколько примеров:
Обычно,
Условный выбор типа с помощью std::conditional
В заголовке
<type_traits> есть шаблон std::conditional, который позволяет нам динамически выбирать тип на этапе компиляции. Он принимает три шаблонных параметра - булево значение, тип для использования, если первый параметр true, и тип, если он false. Результирующий тип доступен в статическом члене type.Несколько примеров:
using namespace std;
// здесь будет int
conditional<true, int, float>::type
// Можем использовать conditional_t для доступа к ::type напрямую
// здесь будет std::string
conditional_t<true, string, char*>
// Булев параметр может быть любым выражением, известным на момент компиляции
// здесь будет std::string
constexpr bool useStdString{true};
conditional_t<useStdString, string, char*>
// здесь будет std::vector<float>
conditional_t<false, vector<int>, vector<float>>
// здесь также будет std::vector<float>
vector<conditional_t<false, int, float>>
Обычно,
std::conditional используется для создания псевдонима/алиаса, который мы затем можем легко использовать в нашем коде:#include <iostream>
#include <type_traits> // for std::conditional
#include <typeinfo> // for typeid
constexpr bool Use64BitIntegers{true};
using integer =
std::conditional<Use64BitIntegers,
std::int64_t,
std::int32_t>::type;
int main() {
integer MyInt{42};
std::cout << typeid(integer).name() << " ("
<< sizeof(integer) << " bytes)";
}
// Output (варианты, зависит от компилятора):
// __int64 (8 bytes)
// l (8 bytes)
🔥2
Начиная с C++17, в лямбде можно захватить this по значению через *this (просто this захватит объект по ссылке):
Копия объекта может понадобиться например для исполнения лямбды в другом потоке, когда this уже может быть инвалидирован на момент вызова лямбды.
https://habr.com/ru/companies/otus/articles/794821/
class MyClass {
public:
int value = 10;
void printValue() {
auto lambda = [=, *this]() {
std::cout << "Value: " << value << std::endl;
};
lambda();
}
};
Копия объекта может понадобиться например для исполнения лямбды в другом потоке, когда this уже может быть инвалидирован на момент вызова лямбды.
https://habr.com/ru/companies/otus/articles/794821/
Хабр
Краткий обзор развития лямбда-выражений в C++11, C++14, C++17 и C++20
Привет, Хабр! Сегодня рассмотрим лямбда-выражения в C++ и их эволюцию с момента появления в стандарте C++11 и до последних обновлений в C++20. Лямбда - выражения в C++ — это анонимные функции, которые...
👍4
Забавная функция из <numeric> появилась в C++11 - называется std::iota, по всей видимости это греческая буква йота (⍳). Так функция называлась в языке программирования APL (вышел в 1966-м, на лого яблоко, впрочем не надкусанное).
Функция заполняет интервал последовательными значениями (++), начиная с переданного.
Функция заполняет интервал последовательными значениями (++), начиная с переданного.
#include <iostream>
#include <numeric> // iota
#include <vector>
int main()
{
std::vector<int> v;
v.assign(10, 0);
std::iota(v.begin(), v.end(), 10);
for (auto i : v) {
std::cout << i << " ";
}
return 0;
}
// Output:
// 10 11 12 13 14 15 16 17 18 19
В стандартной библиотеке есть несколько функций для работы с отсортированными контейнерами. Одна их них std::lower_bound:
Запомнить результат работы можно так: вернет итератор на позицию, перед которой можно вставить value, не нарушив порядок. Если такой позиции нет, то вернет last (обычно end() контейнера).
Так, в примере есть несколько подряд идущих 5, алгоритм вернет итератор на первую (если вставить 5 перед ним, то сортировка не нарушится).
В примере
При поиске элемента, большего всех, вернется конец контейнера, тк мы должны вставить его в конец, чтобы сохранить сортировку.
std::lower_bound(first, last, value) находит первый элемент в интервале [first, last) контейнера, который НЕ расположен до value, то есть не меньше value. Для сравнения используется operator<.Запомнить результат работы можно так: вернет итератор на позицию, перед которой можно вставить value, не нарушив порядок. Если такой позиции нет, то вернет last (обычно end() контейнера).
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> v = {1, 2, 4, 5, 5, 5, 6, 7, 8};
auto print_lower_bound = [&](int x){
auto it = std::lower_bound(v.begin(), v.end(), x);
if (it != v.end()) {
std::cout << "[" << std::distance(v.begin(), it) << "] = "
<< *it << std::endl;
} else {
std::cout << "not found" << std::endl;
}
};
print_lower_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_lower_bound(3); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_lower_bound(0); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_lower_bound(9); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
return 0;
}
// Output:
// [3] = 5
// [2] = 4
// [0] = 1
// not found
Так, в примере есть несколько подряд идущих 5, алгоритм вернет итератор на первую (если вставить 5 перед ним, то сортировка не нарушится).
В примере
2, 4 при поиске 3 будет возвращен итератор не на 2, а на 4, тк это первое значение для которого < 3 не сработает (если вставить 3 перед 4, то сортировка не нарушится).При поиске элемента, большего всех, вернется конец контейнера, тк мы должны вставить его в конец, чтобы сохранить сортировку.
Для того, чтоб найти первый элемент сортированной последовательности, который больше значения, можно применить std::upper_bound. Если сравнить результат работы с lower_bound, то на первый взгляд логика такая же - вернется позиция, перед которой можно вставить элемент, не нарушив сортировки:
Однако, как видим, если есть повторяющиеся элементы, то результат разный:
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> v = {1, 2, 4, 5, 5, 5, 6, 7, 8};
auto print_upper_bound = [&](int x){
auto it = std::upper_bound(v.begin(), v.end(), x);
if (it != v.end()) {
std::cout << "[" << std::distance(v.begin(), it) << "] = "
<< *it << std::endl;
} else {
std::cout << "not found" << std::endl;
}
};
print_upper_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_upper_bound(3); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_upper_bound(0); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_upper_bound(9); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
return 0;
}
// Output:
// [6] = 6
// [2] = 4
// [0] = 1
// not found
Однако, как видим, если есть повторяющиеся элементы, то результат разный:
print_lower_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
print_upper_bound(5); // 1, 2, 4, 5, 5, 5, 6, 7, 8
// ^
👍4
std::string::substr возвращает новую строку, созданную как копию подстроки:basic_string substr( size_type pos = 0, size_type count = npos ) const;Первый параметр - это позиция (index) начала подстроки, а второй - количество символов (а НЕ позиция конца!). Второй параметр можно не передавать и тогда строка скопируется до конца.
Несколько примеров:
#include <iostream>
#include <string>
int main()
{
std::string str =
"In anger we should refrain both"
" from speech and action."; // Pythagoras
// string substr (size_t pos = 0, size_t len = npos) const;
// С позиции (индекса) == 3 взять 5 символов, получим "anger"
std::string strFrom3Len5 = str.substr(3, 5); // копия подстроки
std::cout << strFrom3Len5 << std::endl;
// Найдем позицию с которой "speech" начинается
std::size_t speechPos = str.find("speech");
if (speechPos != str.npos) { // нашли?
// Если не указать длину, то скопируется до конца
std::cout << str.substr(speechPos) // speech and action.
<< std::endl;
}
size_t strLen = str.size();
// Если pos равен длине строки, то вернется пустая строка
std::cout << str.substr(strLen).size() << std::endl; // 0
// Получить подстроку у пустой строки с 0 тоже можно
std::cout << std::string("").substr(0).size() << std::endl; // 0
try
{
// Если же pos больше длины строки,
// то будет исключение std::out_of_range
std::cout << str.substr(strLen + 1).size() << std::endl;
}
catch (const std::out_of_range& ex)
{
std::cout << ex.what() // invalid string position
<< std::endl;
}
// Если длина len больше, чем число оставшихся символов,
// то подстрока идет до конца, без исключения
std::cout << str.substr(speechPos, 1000) // speech and action.
<< std::endl;
}
// Output:
// anger
// speechand action.
// 0
// 0
// invalid string position
// speech and action.
Type traits часть 6 (перевод) - финал!
Мы не ограничены трейтами, которые идут вместе со стандартной библиотекой. Можно взять их из сторонних библиотек или написать свои.
Представим, что у нас есть шаблонная функция, которая получает объекты, которые либо имеют, либо не имеют функцию
Трейты из std обычно предоставляют булево значение в виде поля
Обычно мы хотим
Далее, если для какого-то класса мы хотим значение трейта равное
Теперь с этими четырьмя линиями кода запустим наш трейт:
Вывод:
Мы не ограничены трейтами, которые идут вместе со стандартной библиотекой. Можно взять их из сторонних библиотек или написать свои.
Представим, что у нас есть шаблонная функция, которая получает объекты, которые либо имеют, либо не имеют функцию
render. Напишем трейт is_renderable:template <typename T>
void Function(T Param) {
if constexpr (is_renderable<T>::value) {
Param.Render();
} else {
std::cout << "\nNot Renderable";
}
}
Трейты из std обычно предоставляют булево значение в виде поля
value. Мы можем легко добиться такого же поведения от наших собственных структур, наследуясь от хелперов std::false_type или std::true_type, доступных в заголовке <type_traits>.Обычно мы хотим
value иметь по-умолчанию false, поэтому мы наследуемся от false_type:template <typename>
struct is_renderable : std::false_type {};
Далее, если для какого-то класса мы хотим значение трейта равное
true, мы предоставляем специализацию, которая наследуется от true_type:template <>
struct is_renderable<MyClass> : std::true_type {};
Теперь с этими четырьмя линиями кода запустим наш трейт:
#include <type_traits>
#include <iostream>
template <typename>
struct is_renderable : std::false_type {};
class Fish {
public:
void Render() {
std::cout << "Fish: ><((((`>";
}
};
template <>
struct is_renderable<Fish> : std::true_type {};
class Sound {};
template <typename T>
void Function(T Param) {
if constexpr (is_renderable<T>::value) {
Param.Render();
} else {
std::cout << "\nNot Renderable";
}
}
int main() {
Fish MyFish;
Function(MyFish);
Sound MySound;
Function(MySound);
}
Вывод:
Fish: ><((((`>
Not Renderable
👍1
std::distance(_InIt first, _InIt last) возвращает "расстояние" - число переходов от первого итератора ко второму.Для итераторов со случайным доступом (random access), например итератором для
vector, эта функция будет работать за O(1) и вернет их разность. Казалось бы, мы можем просто сами найти эту разность итераторов и дело с концом, зачем вообще нужна эта функция?Однако, для итераторов без случайного доступа (например
list), расстояние тоже найдется, но уже путем линейного прохода от одного итератора к другому. Просто разность итераторов тут найти не получится, так как для таких итераторов она просто не определена. Время работы функции в таком случае будет O(n). Таким образом std::distance позволяет найти расстояние по любому типу итераторов, как и многое в std.Пример:
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <map>
int main()
{
std::vector<int> v = {10, 20, 30, 40, 50};
std::cout << std::distance(v.begin(), v.end())
<< " " << v.end() - v.begin()
<< std::endl; // 5 5
std::cout << std::distance(
std::next(v.begin()), // ++
std::prev(v.end()) // --
) << std::endl; // 3
auto it = v.begin();
std::advance(it, 3); // продвинемся на 3 вперед
std::cout << std::distance(v.end(), it) << std::endl; // -2
std::list<int> list = { 5, 6, 7, 8 };
std::cout << std::distance(list.cbegin(), list.cend()) << std::endl; // 4
//std::cout << list.end() - list.begin(); // нельзя
std::cout << std::distance(list.cend(), list.cbegin()) << std::endl; // 1 - неверно, тк не random access, UB
std::map<int, int> m = { {1,2}, {3,4}, {5, 6} };
std::cout << std::distance(m.find(3), m.end()) << std::endl; // 2
return 0;
}
// Output:
// 5 5
// 3
// -2
// 4
// 1
// 2
Интересно, что std::distance может вернуть отрицательное значение для random access, если первый итератор дальше второго, что является вполне валидным:
std::distance(v.end(), v.begin()) // -v.size()
Для не random access у меня вернулась ерунда, хотя в таком случае вообще будет UB.