Маша С++
104 subscribers
54 photos
2 files
65 links
Учу C++ и не только вместе с вами, мои котятки)
Download Telegram
При вызове оператора new объекты инициализируются по умолчанию. То есть как для int i; в результате будет мусор, так и для new int; в результате будет мусор. А вот например new string вызовет конструктор по умолчанию, то есть создаст пустую строку.
Для заполнения динамического значения нулем, следует использовать круглые скобки, при этом круглые скобки можно использовать и с динамическими массивами (что создаст массив из нулей):
#include <iostream>

void fill_heap_with_garbage()
{
// повышаем вероятность мусора, а не 0
int* intp = new int(99);
delete intp;

int* arrp = new int[42];
for (size_t i=0; i<42; ++i) arrp[i] = i;
delete[] arrp;
}

int main()
{

fill_heap_with_garbage();

{
int* i1 = new int; // мусор
int* i2 = new int(); // 0
int* i3 = new int(42); // 42

std::cout << *i1 << " " << *i2 << " " << *i3
<< std::endl; // 1660576172 0 42

delete i1;
delete i2;
delete i3;
}

{
int* a1 = new int[42]; // мусор
int* a2 = new int[42](); // нули

std::cout << a1[10] << " " << a2[10]<< std::endl; // 10 0

delete[] a1;
delete[] a2;
}

return 0;
}


Интересно, что передать ненулевое значение для единичного объекта можно, а для массива нельзя:
new int(42); // можно, будет 42
new int[100](42) // нельзя
👍1
Определить массив нулевой длины в C++ нельзя, sizeof такого массива будет 0. Однако же, динамически выделить массив нулевой длины можно:
#include <iostream>

int main()
{
constexpr size_t size = 0;

int a1[0]; // нельзя
int * a2 = new int[size]; // можно

std::cout << sizeof(a1) << " " << size_t(a2) << std::endl; // 0 94376700628656

for (int* i = a2; i != a2 + size; ++i ) {
std::cout << *i;
}

delete[] a2;

return 0;
}

Такой указатель на пустой массив не будет нулевым, но он будет похож на указатель на элемент после конца массива.
Такой указатель можно сравнивать, добавить и вычесть 0, вычесть из себя. Поэтому цикл выше вполне валиден (он кстати не выполнится).
Конечно же, обращаться к значению такого указателя нельзя.
👍3
Как правило, конструктор копирования (copy constructor) принимает как параметр константную ссылку на свой же тип: Class(const Class & other).
В реальности же, первый параметр может быть обычной (не константной) ссылкой на свой тип (хотя смысла в этом обычно нет). Более того, конструктор копирования может принимать и другие аргументы, главное чтоб они имели значение по умолчанию. Пример:
#include <iostream>

class A
{
public:
A() = default;
A(A & other, int i = 10)
{
std::cout << "copy ctor " << i << std::endl;
}
};

void f(A a) // makes copy
{}


int main()
{
A a;
f(a);

return 0;
}


И тут возникает вопрос, а может и ссылка не обязательна? Что если передавать первый аргумент по значению: Class(Class other) ?
Но так делать нельзя, так как для создания копии параметра конструктора копии понадобится вызвать этот же конструктор копии, то есть будет рекурсия. Поэтому первый параметр должен передаваться по ссылке.
👍6🤔1
Тут на собеседовании спросили какие умные указатели бывают, конечно же unique_ptr, shared_ptr и weak_ptr, про это все знают.
Зачем же нужны weak_ptr? Давайте посмотрим на такой вроде бы игрушечный пример:
#include <iostream>
#include <memory>

class A
{
public:
std::shared_ptr<A> parent;
std::shared_ptr<A> callback;
~A()
{
std::cout << "~A" << std::endl;
}
};

void shared_cycle()
{
std::shared_ptr<A> parent = std::make_shared<A>();
std::shared_ptr<A> child = std::make_shared<A>();
child->parent = parent;
parent->callback = child; // без этой строчки будет ~A ~A
}

int main()
{
shared_cycle();
return 0;
};


Из-за циклической зависимости двух объектов деструктор не вызовется!
Конечно же в реальности объекты могут не находиться в коде "рядом" или цикл может быть подлиннее.
Спасением может оказаться weak_ptr, если мы поменяем одну из переменных с shared на weak:
std::weak_ptr<A> callback;
👍52
На реддите пишут, что на собесах иногда спрашивают про CRTP. А вы знали, что это такое?)

CRTP (Curiously Recurring Template Pattern, "любопытный повторяющийся шаблон") - это идиома C++, в которой класс наследуется от реализации шаблонного метода, используя сам себя в качестве шаблонного аргумента.
Пример (из википедии 😁) :
template <class T> 
struct Base
{
void interface()
{
// ...
static_cast<T*>(this)->implementation();
// ...
}
};

struct Derived : Base<Derived>
{
void implementation();
};


Как видим, Derived наследует Base и передает сам себя в качестве шаблона. Отсюда и рекурсия. Немного напоминает class Derived : public std::enable_shared_from_this<Derived>.

Зачем это нужно? Так можно достигнуть поведения, аналогичного виртуальным функциям, но без динамического полиморфизма.

Дальнейшее объяснение слишком длинное, предлагаю для подробностей обратиться к хабру.
🔥21👍1
shared_ptr обычно реализуется через подсчет ссылок - количества объектов, которые ссылаются на указатель. При копировании shared_ptr количество увеличивается на 1, деструктор уменьшает количество на 1. При достижении числа 0 вызывается deleter (по-умолчанию delete).

Для иллюстрации воспользуемся методом 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 и контрольный блок уничтожится).
🔥21👎1
В прошлый раз я писала, что make_shared разместит объект прямо в управляющем блоке, а также что управляющий блок не разрушится, если уничтожены все shared_ptr, но есть хотя бы один weak_ptr, отслеживающий этот контрольный блок. Это значит, что после разрушения последнего шарного указателя память может значительное время не освобождаться (повторюсь, при использовании make_shared).
Давайте проведем небольшой тест: создадим строку размером 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>:

#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 вычисляется во время компиляции, а не выполнения. Когда мы хотим выполнить проверку чего-то, известного во время компиляции, то мы можем добавить 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 (перевод)

Рассмотрим такую простую шаблонную функцию:
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
Задачка для собеса. Пусть дан вектор беззнаковых целых, в которых есть нули. Нужно сдвинуть все нули в конец вектора, то есть написать код функции, используя стандартные алгоритмы:
#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 и вернет.
👍31
cpp_books_roadmap.png
951.3 KB
Роадмап для изучения C++ (и не только) из книжек. Взято тут.
🔥8🤔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 это параметризированная структура) содержит поле 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

#юмор
❤‍🔥1😁1
Type traits часть 5 (перевод)

Условный выбор типа с помощью 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
software_development_books.png
520.3 KB
Роадмап из книжек про разработку вцелом. Источник.
💘2
Начиная с C++17, в лямбде можно захватить this по значению через *this (просто this захватит объект по ссылке):
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/
👍4