https://www.youtube.com/watch?v=i_wDa2AS_8w&list=PLJ_usHaf3fgM5vOBPY-hXAjUy6SbgE-KG
이 글은 위 영상을 통해 만들어졌음을 알립니다.
영문 주석은 이렇게 하지 마라~라는 의미를 주로 담고 있고, 한국어는 의역을 통해 이렇게 해라~로 작성하였습니다.
1. using namespace std를 헤더에 사용하지 맙시다.
- 다른 사람이 당신의 코드를 사용할 때 namespace로 인한 문제가 발생할 수 있다.
// 1. don't using namespace std to header
using std::string, std::cout, std::endl;
2. std::endl를 사용하지 말자, 특히 루프에서
// 2. don't using std::endl especially in a loop
std:: endl과 \n의 차이
std::endl과 \n의 차이는 여기서 나타난다. std::endl은 위의 2번에 해당하는 문자다. std::endl이 입력되면 버퍼는 자동으로 비워진다.
\n의 경우 버퍼를 비우지 않는다. 다만 구현체에 따라서 std::endl처럼 버퍼를 비우도록 처리하도록 하는 경우도 있다. 그렇지만 장담할 수 없으므로, 버퍼를 비우고 싶으면 std::endl을 사용해야 한다.
속도도 차이난다. 버퍼를 비우는 std::endl 이 느리고, 비우지 않는 \n 이 빠르다. 굳이 즉시 출력해 줘야 하는 게 아니라면, \n으로 모아뒀다가 출력하는 게 시간을 줄일 수 있다. 백준에서 타임아웃 문제로 이 둘의 차이를 궁금해하는 사람들이 많다.
3. 의도를 표현하는 반복문은 범위 기반 for 루프를 사용합시다.
- 가시성을 위해서 의미있는 이름으로 변수를 선언해 줍니다.
// 3. using a for loop by index when a range-based for loop expresses the intent better.
// worse case
void train_model(const std::vector<int> &data, auto &model)
{
for (std::size_t i = 0; i < data.size(); ++i)
{
model.update(data[i]);
}
}
// good case
void train_model(const std::vector<int> &data, auto &model)
{
for (const auto &x : data)
{
model.update(x);
}
}
4. 새로 만드는 것보다 존재하는 알고리즘을 사용합시다.
// 4. using a loop when a standard algorithm already exists to do what you're trying to do
// worse case
void know_your_algorithms()
{
const std::vector<int> data{-1, -3, -5, 8, 15, -1};
std::size_t first_pos_idx;
for (std::size_t i = 0; i < data.size(); i++)
{
if (data[i] > 0)
{
first_pos_idx = i;
break;
}
}
}
// good case
void know_your_algorithms()
{
const std::vector<int> data{-1, -3, -5, 8, 15, -1};
const auto is_positive = [](const auto &x)
{ return x > 0; };
auto first_pos_it = std::find_if(data.begin(), data.end(), is_positive);
}
5. 표준 배열을 사용합시다.
- C 스타일 배열은 종종 배열의 길이를 별도로 전달해야 합니다.
- 이때 Array를 사용하면 추가로 전달할 필요가 없습니다.
// 5. using a C style array when you could have used a standard array.
// worse case
void f(int *arr, int n)
{
// whatever
}
void using_c_array()
{
const int n = 256;
int arr[n] = {0};
f(arr, n);
}
// good case
template <std::size_t size>
void better_f(std::array<int, size> &arr)
{
}
void using_c_array()
{
const int n = 256;
std::array<int, n> arr{};
better_f(arr);
}
6. reinterpret_cast를 사용하지 맙시다.
TODO : reinterpret_cast에 대한 글쓰기
- reinterpret_cast는 데이터의 비트를 그대로 유지하면서, 한 타입의 포인터를 다른 타입의 포인터로 변환하거나, 정수 타입을 포인터 타입으로 변환하는 데 사용됩니다. 이러한 변환은 종종 정의되지 않은 행동(Undefined Behavior)을 초래할 수 있습니다.
- C++20에서는 bit_cast가 도입되었습니다. bit_cast는 타입 안전성을 유지하면서 한 타입의 객체를 다른 타입으로 해석할 수 있게 해 줍니다. 이는 reinterpret_cast의 안전하지 않은 사용을 대체할 수 있는 좋은 방법입니다.
// 6. any use of reinterpert_cast.
// worse case
void any_use_of_reinterpret_cast()
{
long long x = 0;
auto xp = reinterpret_cast<char *>(x);
auto x2 = reinterpret_cast<long long>(xp);
}
// good case
void any_use_of_reinterpret_cast()
{
float y = .123f;
long i = *(long *)&y; // Sorry famous Quake III inv_sqrt code
y = *(float *)&i;
}
7. const를 없애지 맙시다.
- const map을 사용할 때 객체에 접근을 위해 const_cast를 통해 const를 없애면 자료가 훼손될 수 있습니다.
- at을 사용해서 const를 유지하면서 접근해 주세요.
// 7. casting away const
// worse case (const_cast를 사용하여 const를 제거)
const std::string &more_frequent(const std::unordered_map<std::string, int> &word_counts,
const std::string &word1,
const std::string &word2)
{
auto &counts = const_cast<std::unordered_map<std::string, int> &>(word_counts);
return counts[word1] > counts[word2] ? word1 : word2;
}
// good case (at 함수를 사용하여 안전하게 값을 조회)
const std::string &more_frequent(const std::unordered_map<std::string, int> &word_counts,
const std::string &word1,
const std::string &word2)
{
return word_counts.at(word1) > word_counts.at(word2) ? word1 : word2;
}
8. map에 []를 사용하면 없는 key값의 경우 value 0이 생성됩니다.
- 이는 의도치 않은 key의 생성을 하니 주의하세요
// 8. not knowing map bracket inserts element
// worse case
const std::string &more_frequent(const std::unordered_map<std::string, int> &word_counts,
const std::string &word1,
const std::string &word2)
{
return word_counts[word1] > word_counts[word2] ? word1 : word2;
}
9. const 구문을 사용합시다.
- 사용자가 내용을 수정하지 않음을 쉽게 확인할 수 있습니다.
// 9. ignoring const correctness
// worse case
void print_vec_one_per_line(std::vector<int> &arr)
{
for (const auto &x : arr)
{
std::cout << x << '\n';
}
}
// good case
void print_vec_one_per_line(const std::vector<int> &arr)
{
for (const auto &x : arr)
{
std::cout << x << '\n';
}
}
10. 문자열 리터럴은 프로그램의 전체 수명 동안 유효한 상수 메모리 영역에 저장됩니다.
- 따라서 지역 변수를 참조하는 것처럼 보이지만 반환하는 것은 완벽하게 괜찮습니다.
// 10. not knowing about string literal lifetimes
// good case
const char *string_ligeral_lifetimes()
{
return "string literals";
}
문자열 리터럴의 수명에 대한 중요한 점
문자열 리터럴은 컴파일 시점에 정의되며, 실행 파일의 데이터 섹션에 저장됩니다. 이들은 프로그램이 실행되는 동안 계속 유효하며, 프로그램이 종료될 때까지 메모리에 남아 있습니다. 문자열 리터럴은 상수(const)로 취급되므로, 이들을 가리키는 포인터는 const char * 타입이어야 합니다. 문자열 리터럴을 가리키는 포인터를 반환하는 것은 안전하며, 이는 로컬 변수가 아니라 정적 메모리 영역을 가리킵니다.
11. 구조화 바인딩을 사용하여 가독성을 높입시다.
TODO : 구조화 바인딩에 대한 글 쓰기
- 이 방법은 코드를 더 읽기 쉽고 이해하기 쉽게 만들어 줍니다.
- 구조화된 바인딩은 구성원이 공개된 경우 자신의 유형과 함께 사용할 수도 있습니다.
- 이름은 선언 순서에 따라 변수에 할당됩니다.
// 11. not using structured bindings.
// worse case
void loop_map_items()
{
std::unordered_map<std::string, std::string> colors = {
{"RED", "#FF0000"},
{"GREEN", "#00FF00"},
{"BLUE", "#0000FF"}};
for (const auto &pair : colors)
{
std::cout << "name: " << pair.first << ", hex : " << pair.second < < < '\n';
}
}
// good case
void loop_map_items()
{
std::unordered_map<std::string, std::string> colors = {
{"RED", "#FF0000"},
{"GREEN", "#00FF00"},
{"BLUE", "#0000FF"}};
for (const auto &[name, hex] : colors)
{
std::cout << "name: " << name << ", hex : " << hex < < < '\n';
}
}
12. 여러 개의 값을 반환할 경우, out 매개변수 대신 구조체를 만들어서 반환합시다.
- 사용자가 확인하기 편합니다.
// 12. using multiple out parameters when you want to return multiple things form a function
// worse case
void get_values_out_params(const int n, int &out1, int &out2)
{
out1 = n;
out2 = n + 1;
}
// good case
struct Values
{
int x, y;
};
Values get_values_out_params(const int n)
{
return {n, n + 1};
}
13. 컴파일 시간에 수행할 수 있는 것은 수행합시다. (constexpr)
- 런타임 성능을 향상 시켜줄 수 있습니다.
// 13. doing word at runtime that could have been done at compile time.
// worse case
int sum_of_1_to_n(const int n)
{
return n(n + 1 / 2);
}
void uses_sum()
{
const int limit = 1000;
auto triangle_n = sum_of_1_to_n(limit);
// use triangle_n...
}
// good case
constexpr int sum_of_1_to_n(const int n)
{
return n(n + 1 / 2);
}
void uses_sum()
{
const int limit = 1000;
auto triangle_n = sum_of_1_to_n(limit);
// use triangle_n...
}
14. 기초 클래스의 ~ 소멸자에 virtual 키워드를 까먹지 맙시다.
- 없을 경우 파생 클래스 소멸자는 호출되지 않으며 기본 클래스 소멸자만 호출됩니다.
15. 클래스 멤버는 목록의 순서대로가 아니라 변수가 선언된 순으로 초기화됩니다.
- 변수의 선언 순에 따라 컴파일 에러가 발생할 수 있습니다.
// 15. thinking that class members are initialized in the order they apear in the initializer list.
// worse case
class View
{
public:
View(char *start, std::size_t size) : m_start{start}, m_end{m_start + size} {};
private:
char *m_end;
char *m_start;
}
// good case
class View
{
public:
View(char *start, std::size_t size) : m_start{start}, m_end{m_start + size} {};
private:
char *m_start;
char *m_end;
};
16. 초기화를 잊지 맙시다.
- 초기화를 하지 않으면 쓰레기 값이 프로그램을 망칩니다.
// 16. not realizing there's a difference between default and value initialization
void default_vs_value_initialization()
{
// worse case
int x;
int *x2 = new int;
// good case
int y{};
int *y2 = new int{};
int *y3 = new int();
}
17. 대체될 수 있는 숫자를 의미 있는 이름의 상수로 바꿉시다.
- 가독성이 좋아집니다.
// 17. overuse of magic numbers.
// worse case
float energy(float m)
{
return m * 299792458.0 * 299792458.0;
}
// good case
float energy(float m)
{
constexpr float SPEED_OF_LIGHT = 299792458.0;
return m * SPEED_OF_LIGHT * SPEED_OF_LIGHT;
}
18. 범위지정 반복문을 사용할 때 객체에 추가와 삭제를 하지 맙시다.
- vector를 통해 범위 지정 연산을 하고 있을 때 값이 추가되면(push_back) vector의 reserve된 공간을 초과하여 위치를 이동할 수도 있습니다.
- 이때, 범위지정 반복문에 사용되는 Iterator는 이를 반영하지 않으므로 오류가 발생할 수 있습니다.
// 18. attempting to add or remove elements form a container while looping over it
// worse case
void modify_while_iterating()
{
std::vector<int> v{1, 2, 3, 4};
for (auto x : v)
{
v.push_back(x);
}
for (auto x : v)
{
std::cout << x << ' ';
}
}
// good case
void modify_while_iterating()
{
std::vector<int> v{1, 2, 3, 4};
for (auto it = v.begin(), end = v.end(); it != end; ++it)
{
v.push_back(*it);
}
/*
// Or do this
const std::size_t size = v.size();
for(std::size_t i = 0; i < size; ++i)
v.push_back(v[i]);
*/
for (auto x : v)
{
std::cout << x << ' ';
}
}
19. 함수 반환값에 move()를 사용하지 말자
TODO : 반환 값 최적화(Return Value Optimization, RVO) 글쓰기
- RVO는 컴파일러가 함수에서 반환되는 객체를 직접 반환 위치에 생성하여, 불필요한 복사나 이동을 피할 수 있게 하는 기술입니다. 이 최적화는 컴파일러가 자동으로 수행할 수 있으며, 특히 C++11 이후에 더욱 강화되었습니다.
코드에서 로컬 객체에 std::move를 사용해 반환하는 것은 종종 불필요하며, 심지어 RVO를 방해할 수 있습니다. std::move는 컴파일러에게 해당 객체가 더 이상 사용되지 않으며 이동 가능하다는 것을 알리지만, 이는 컴파일러가 이미 알고 있는 정보입니다. 더욱이, 명시적으로 std::move를 사용함으로써 컴파일러가 RVO를 수행하는 것을 방해할 수 있으며, 이는 불필요한 이동 생성자 호출을 발생시킬 수 있습니다.
따라서, 함수에서 로컬 변수를 반환할 때는 std::move를 사용하지 않고 직접 반환하는 것이 좋습니다. 이렇게 하면 컴파일러가 필요한 최적화를 수행할 수 있는 기회를 제공할 수 있습니다.
// 19. returning a moved local variable.
// worse case
std::vector<int> make_vector(const int n)
{
std::vector<int> v{1, 2, 3, 4, 5};
return std::move(v);
}
// good case
std::vector<int> make_vector(const int n)
{
std::vector<int> v{1, 2, 3, 4, 5};
return v;
}
20. move()는 정확히는 rvalue로 static casting 하는 것이다.
- move는 무언가를 삭제하는 것이 아님을 알자.
// 20. thinking that move actually moves somthing.
// Implementation of standard move
template <typename T>
constexpr std::remove_reference_t<T> &&
move(T &&value) noexcept
{
return static_cast<std::remove_reference_t<T> &&>(value);
}
constexpr int &&
move(int &value) noexcept
{
return static_cast<int &&>(value);
}
21. 서브 표현식(sub-expression)의 실행 순서는 일관적이지 않다. (Before C++17)
- C++17 이전의 동작: C++17 이전에는 컴파일러가 표현식 내의 서브 표현식을 실행하는 순서가 명확히 정의되어 있지 않았습니다. 이는 코드가 의도한 대로 작동하지 않을 수 있는 여지를 만들었습니다. 예를 들어, 문자열에서 연속적인 replace 연산을 수행할 때, 첫 번째 replace가 수행된 후 문자열의 내용이 변경되면, 두 번째 replace가 예상치 못한 결과를 초래할 수 있습니다.
- C++17 이후의 변경: C++17부터는 이러한 서브 표현식의 평가 순서가 명확히 정의되어, 이와 같은 코드의 동작이 보장됩니다. 이는 코드의 예측 가능성과 안정성을 향상시킵니다.
- 함수 인자의 평가 순서: 하지만, C++20에서도 함수 인자가 평가되는 순서는 여전히 왼쪽에서 오른쪽으로 일관되게 보장되지 않습니다. 이는 함수 인자로 전달되는 표현식이 서로 의존하는 경우에 주의가 필요함을 의미합니다. 함수 인자가 순수 함수인 경우(부작용이 없고 입력값만 결과에 영향을 미치는 경우)에는 이러한 평가 순서가 큰 문제가 되지 않습니다.
// 21. thinking that evaluation order is guaranteed to be left to right
// 서브 표현식(sub-expression) 평가 순서
void function_evaluation_order_not_guaranteed()
{
std::string str = "but i have heard it works even if you don't believe in it";
str.replace(0, 4, "")
.replace(str.find("even"), 4, "only")
.replace(str.find("don't"), 6, "");
std::string expected = "i have heard it works only if you beliebe in it";
std::string ErrorExpected = "i have heard it works evenonlyyou don't believe in it";
}
// 함수 인자의 평가 순서
int a();
int b();
int c();
int g(int, int, int);
void function_evaluation_order_not_guaranteed()
{
g(a(), b(), c());
}
22. 불필요한 힙 할당하지 않기 (객체의 크기가 작을 때)
- 스택 할당에 비해 힙 할당이 속도가 느립니다.
- 따라서, 불필요하다면 성능을 위한 스택 할당을 해줍시다.
// 22. using totally unnecessary heap allocations when a stack allocation would have been fine
// worse case
struct Record
{
int id;
std::string name;
};
void necessary_heap_allocations()
{
Record *customer = new Record{0, "James"};
Record *other = new Record{1, "Someone"};
delete customer;
delete other;
}
// good case
struct Record
{
int id;
std::string name;
};
void necessary_heap_allocations()
{
Record customer{0, "James"};
Record other{1, "Someone"};
}
23. 힙을 사용할 때 unique_ptr과 shared_ptr를 사용하자.
- 스마트 포인터는 메모리 누수를 막기 위한 좋은 자료형이다.
- delete를 사용하지 않더라도 stack(포인터)가 사라지면 heap(data)도 같이 사라져 메모리 누수를 방지한다.
- unique_ptr은 리소스에 대한 단일 소유권을, shared_ptr은 공유 소유권을 나타냅니다.
// 23. not using unique pointer and shared pointer to do your heap allocations
// worse case
struct Record
{
int id;
std::string name;
};
void necessary_heap_allocations()
{
Record *customer = new Record{0, "James"};
Record *other = new Record{1, "Someone"};
// do work EXCEPTION
delete customer;
delete other;
}
// good case
struct Record
{
int id;
std::string name;
};
void necessary_heap_allocations()
{
auto *customer = std::unique_ptr<Record>(new Record{0, "James"});
auto *other = std::unique_ptr<Record>(new Record{1, "Someone"});
}
24. unique_ptr로 만들지 말고 make_unique를 사용하자. (shared도)
- 보다 안전한 코드: std::make_unique와 std::make_shared는 객체를 생성하고 적절한 스마트 포인터로 바로 감싸주므로, 생명주기 관리가 보다 안전해집니다. 직접 new를 사용하여 생성한 후 스마트 포인터에 할당하는 것보다 이 방식이 예외 처리에 있어서 더 안전합니다.
- 보다 간결한 코드: 이 함수들을 사용하면 생성자에 직접 인자를 전달할 수 있으므로 코드가 간결해집니다.
- 성능 최적화: 특히 std::make_shared는 객체와 참조 카운트를 동일한 메모리 블록에 할당하여 성능 최적화를 제공합니다. 이는 별도로 할당할 때보다 메모리 오버헤드가 적고 효율적입니다.
- 단, 생성자를 선언해 주어야 합니다.
// 24. constructing a unique or shared pointer directly instead of using make unique or make shared.
struct Record
{
int id;
std::string name;
Record(int id, std::string name) : id{id}, name{name} {};
};
void necessary_heap_allocations()
{
auto *customer = std::make_unique<Record>(0, "James");
auto *other = std::make_unique<Record>(1, "Someone");
}
25. 제발 스마트 포인터 쓰세요.
- 자동 메모리 관리: std::unique_ptr의 소멸자가 자동으로 메모리를 해제합니다.
- 예외 안전성: 객체 생성 중 예외가 발생해도 std::unique_ptr가 자동으로 메모리를 정리합니다.
- 코드 간결성: 메모리 관리에 관한 코드가 필요 없어 코드가 간결해집니다.
// 25. any use of new or delte.
// worse case
struct SomeResource
{
};
class Widget
{
public:
Widget() : meta{new SomeResource} {}
virtual ~Widget()
{
delete meta;
}
private:
SomeResource *meta;
}
// good case
struct SomeResource
{
};
class Widget
{
public:
Widget() : meta{std::make_unique<SomeResource>()} {}
private:
std::unique_ptr<SomeResource> meta;
}
26. RAII를 따르는 기능을 사용합시다. ( std::unique_ptr, std::shared_ptr, std::ifstream, std::ofstream etc)
- 이것은 new와 delete를 사용하는 것과 거의 같은 이야기입니다.
- 만약 여러분이 수동으로 자원을 해제하거나 닫거나 반환하는 작업을 하고 있다면, 그것을 자동으로 해주는 클래스가 있는지 살펴보세요.
- 그런데, 소멸자에서 자원이 자동으로 닫히도록 하는 이 아이디어를 RAII라고 합니다.
- 이것은 Resource Acquisition Is Initialization의 약자입니다.
- 하지만 이것은 실제로는 파괴 시 자원이 해제되도록 보장하는 데 더 많이 관련되어 있습니다.
- std::ifstream은 RAII를 따릅니다. ( 소멸자가 파일을 자동으로 닫습니다. )
// 26. any kind of attempt to do manual resource management.
// worse case
void
read_from_a_file(char *name)
{
FILE *fp = fopen(name, "r");
// ... work with file, EXCEPTION?
fclose(fp);
}
// good case
void read_from_a_file(char *name)
{
std::ifstream input{name};
// file WILL be closed
}
27. 원시 포인터가 나쁜 것은 아니다.
TODO : 비공개 unique_ptr 글 보충하기
std::unique_ptr<int, FreeDeleter>
- 예시: max 함수:
- 원시 포인터를 사용하는 max 함수는 두 포인터 중에서 가리키는 값이 더 큰 것을 반환합니다. 이 함수는 포인터가 가리키는 객체들의 생명주기를 관리하지 않으므로, 원시 포인터 사용이 적절합니다.
const int *max(const int *a, const int *b) {
return *a > *b ? a : b;
}
- 함수와 객체 소유권:
- 함수가 객체의 소유권이나 생명주기 관리와 무관할 경우, 스마트 포인터 대신 원시 포인터를 사용하는 것이 더 적합할 수 있습니다.
- C 코드와의 상호 운용성:
- C 언어 코드와 상호 작용할 때는 C의 규칙에 따라야 합니다. C 함수가 힙 할당된 메모리를 반환하고 그 메모리의 해제를 호출자에게 기대하는 경우가 이에 해당합니다.
- C 함수로부터 반환된 메모리를 관리하기 위해 std::unique_ptr를 사용할 수 있지만, C++의 delete 대신 C의 free를 사용해야 합니다. 이를 위해 사용자 정의 해제자(FreeDeleter)를 std::unique_ptr와 함께 사용할 수 있습니다.
- 스마트 포인터와 사용자 정의 해제 함수
struct FreeDeleter {
void operator()(void *x) { free(x); }
};
void do_work() {
auto data = std::unique_ptr<int, FreeDeleter>(some_c_function());
}
28. 객체가 공유될 것인지 확실하지 않을 때 std::unique_ptr을 반환합시다.
- unique_ptr를 shared_ptr로 변환하는 것은 저렴하고 쉽습니다.
- std::move를 사용하여 unique_ptr의 소유권을 shared_ptr로 옮길 수 있습니다.
- unique_ptr의 반환 값을 shared_ptr에 직접 할당할 수도 있습니다.
- unique_ptr은 참조 카운팅이 필요 없어 shared_ptr에 비해 오버헤드가 적습니다.
// 28. returning a shared pointer when you aren't sure the object is going to be shared.
// worse case
struct Pizza
{
/* data */
};
std::unique_ptr<Pizza>
make_unique_pepperoni_pizza(float diameter)
{
std::vector<std::string> toppings = {"red sauce", "cheese", "pepperoni"};
return std::make_unique<Pizza>(diameter, std::move(toppings));
}
void convert_unique_to_shared_is_easy_and_cheap()
{
auto pizza = make_unique_pepperoni_pizza(16.0f);
std::shared_ptr<Pizza> shared_pizza = std::move(pizza);
}
// good case
struct Pizza
{
/* data */
};
std::unique_ptr<Pizza>
make_unique_pepperoni_pizza(float diameter)
{
std::vector<std::string> toppings = {"red sauce", "cheese", "pepperoni"};
return std::make_unique<Pizza>(diameter, std::move(toppings));
}
void convert_unique_to_shared_is_easy_and_cheap()
{
std::shared_ptr<Pizza> shared_pizza = make_unique_pepperoni_pizza(16.0f);
}
29. shared_ptr은 스레드 사용 시 주의해야 합니다. ( 카운팅 메커니즘이 보호되지 않습니다. )
- 참조 카운팅의 스레드 안전성: std::shared_ptr는 내부적으로 참조 카운팅을 사용하여 객체의 수명을 관리합니다. 이 참조 카운팅 메커니즘은 스레드 안전하게 구현되어 있어, 여러 스레드에서 shared_ptr 인스턴스를 공유하더라도 메모리 누수나 이중 해제 같은 문제는 발생하지 않습니다.
- 객체 접근의 스레드 안전성 문제: 하지만 std::shared_ptr를 통해 접근하는 실제 객체의 멤버 변수나 함수에 대한 접근은 자동으로 스레드 안전하게 되지 않습니다. 예를 들어, 여러 스레드가 std::shared_ptr를 통해 동일한 객체의 멤버 변수를 동시에 수정하려고 하면 데이터 레이스가 발생할 수 있습니다.
- 데이터 레이스 해결 방법: 객체의 멤버에 안전하게 접근하기 위해서는 뮤텍스, 락, 원자적 연산 등을 사용하여 동시 접근을 동기화해야 합니다. 이는 std::shared_ptr와는 별개의 문제이며, 개발자가 별도로 처리해야 합니다.
// 29. thinking that shared pointer is thread-safe.
struct Resource
{
int x{};
};
void worker(std::shared_ptr<Recsource> noisy)
{
for (int i = 0; i < 50000; ++i)
{
noisy->x++;
}
}
void shared_ptr_is_NOT_threadsafe()
{
auto r = std::make_shared<Resource>();
std::jthread t2(worker, r);
std::jthread t1(worker, r);
r.reset();
}
30. const 구문 확실하게 암기하기
- 규칙은 const가 바로 왼쪽에 있는 것에 적용된다는 것입니다.
- 왼쪽 끝에 있을 경우, 오른쪽에 있는 것에 적용됩니다.
void const_pointer_vs_pointer_to_const()
{
int x = 0;
int y = 0;
// 그래서 여기서 const는 int에 적용되고 포인터에는 적용되지 않습니다.
const int *ptr1 = &x;
// 여기서도 const는 int에 적용되고 포인터에는 적용되지 않습니다.
int const *ptr2 = &x;
// 그리고 여기서 const는 포인터에 적용되고 int에는 적용되지 않습니다.
int *const ptr3 = &x;
}
설명
- const int *ptr1과 int const *ptr2: 이 두 선언은 동일한 의미를 갖습니다. 두 경우 모두 ptr1과 ptr2는 int에 대한 const 포인터입니다. 즉, 이 포인터들을 통해 가리키는 int 값을 변경할 수 없습니다. 하지만 포인터 자체의 값을 다른 주소로 변경하는 것은 가능합니다.
- int *const ptr3: 이 선언은 ptr3가 const 포인터임을 의미합니다. 즉, ptr3는 초기화된 이후에 다른 주소를 가리키도록 변경할 수 없습니다. 그러나 ptr3가 가리키는 int 값 자체는 변경할 수 있습니다.
31. 컴파일러 에러를 무시하지 마라
- 그것은 '중요'하니까...