31 nooby C++ habits you need to ditch - YouTube
この動画の解説が非常に参考になったので、勉強がてら紹介記事を書くことにしました。
完全翻訳ではなく、大雑把に要点のみ解説しています。気になる方は元動画をご参照ください。
登場するソースコードは下記のURLで入手可能です(解説コメント付き)。
翻訳内容に若干自信がない部分があるので、おかしな点があればご指摘いただけると助かります。
1. using namespace std; 1
#include ...
using namespace std;
void using_namespace_std() {
string s{"hello, world!"};
cout << s << endl;
}
関数などのスコープ内に収まっている場合は問題ないが、グローバル空間やヘッダーにusing
宣言が定義されている場合、名前空間の衝突の原因になるので、このような使い方は避けた方が良い。2
また、できればオブジェクト単位で個別に指定することを検討した方が良い。
#include ...
using std::string, std::cout, std::endl;
void using_namespace_std() {
string s{"hello, world!"};
cout << s << endl;
}
2. (ループの中などでの)std::endl
の複数回使用
void print_range(int start, int end) {
for (auto i = start; i != end; ++i)
std::cout << i << std::endl;
}
std::endl
は実行毎に出力バッファの内容を逐一flushするので、実行時オーバーヘッドを減らすため、ループの途中などflushの必要がない箇所では改行文字を使用した方が良い。
void print_range(int start, int end) {
for (auto i = start; i != end; ++i)
std::cout << i << '\n';
}
3. range-based forを使用すべき箇所でのindexループの使用
void train_model(const std::vector<int> &data, auto &model) {
for (std::size_t i = 0; i < data.size(); ++i) {
model.update(data[i]);
}
}
typoやバグを避けるため、ループカウンタを参照する必要がないのであればrange-based forを使用した方が良い。
void train_model(const std::vector<int> &data, auto &model) {
for (const auto &x: data)
model.update(x);
}
index変数を使う必要がなくなり、typoやバグを仕込む可能性が少なくなる。
4. STLアルゴリズムの再発明
void know_your_algorithms() {
const std::vector<int> data = {-1, -3, -5, 8, 15, -1};
// find the index of the first positive element
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;
}
}
// use first_pos_idx
}
実行しようとしている処理が単純な場合、すでに用意されているSTLアルゴリズムで代用できないか確かめた方が良い。
上記の例(先頭に最も近い正の要素のindexを取得)の場合、std::find_if
関数が使用可能。
void know_your_algorithms() {
const auto is_positive = [](const auto &x) {
return x > 0;
};
auto first_pos_it = std::find_if(
data.cbegin(),
data.cend(),
is_positive);
// or ranges even better
// use first_pos_it
}
5. Cスタイルの配列の使用
void f(int *arr, int n) {
// ...
}
Cスタイルの配列を引数として渡した場合、先頭要素を示すポインタ型にキャストされるため、要素数を別途指定する必要がある。
代わりにstd::array
を使用した方が良い。
template<std::size_t size>
void better_f(std::array<int, size> &arr) {
// ...
}
イテレータや、C++20であればstd::span
を使えば範囲を自由に指定できるようになる。
6. reinterpret_cast
の使用
template<typename T>
void print_bytes(const T &input, std::ostream &os = std::cout) {
auto *bytes = reinterpret_cast<const std::byte *>(&input);
os << "[";
os << std::hex << std::noshowbase;
for (std::size_t i = 0; i < sizeof(T); ++i) {
if (i != 0)
os << " ";
os << std::setfill('0') << std::setw(2);
os << static_cast<int>(bytes[i]);
}
os << "]\n";
}
C++において、reinterpret_cast
した変数を安全に参照できるのは元の型に再変換した場合のみであり、それ以外のほとんどのケースでの参照結果は未保証である。3
C++20でビット列を維持したまま型変換するstd::bit_cast
が実装されたので、変数のビット表現を得たい場合はそちらを使用した方が良い。
template<typename T>
void print_bytes_cpp20(const T &input, std::ostream &os = std::cout) {
using bytearray = std::array<std::byte, sizeof(T)>;
const auto &bytes = std::bit_cast<bytearray, T>(input);
os << "[";
os << std::hex << std::noshowbase;
for (std::size_t i = 0; i < sizeof(T); ++i) {
if (i != 0)
os << " ";
os << std::setfill('0') << std::setw(2);
os << static_cast<int>(bytes[i]);
}
os << "]\n";
}
下記の様なケースにおいて、Cスタイルのキャストはreinterpret_cast
として再解釈される可能性があるため、非推奨。
void any_use_of_reinterpret_cast() {
float y = .123f;
long i = *(long *) &y;
// ...
y = *(float *) &i;
}
7. 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;
}
const std::map
系のコンテナで[]
演算子が使えない場合、const属性をキャストで除去するのではなくat
メソッドを使用した方が良い。
[]
演算子がconst参照でない理由については次項を参照。
8. std::unordered_map
の[]
演算子の挙動に対する誤解
std::unordered_map<std::string, int>
count_words(const std::vector<std::string> words) {
std::unordered_map<std::string, int> counts;
for (const auto &word: words)
counts[word]++;
return counts;
}
std::map
系のコンテナに対して[]
演算子を用いて存在しないキーを参照した場合、valueをデフォルト値で初期化した上で要素を新規作成するため、[]
演算子はconst属性ではない。
9. const
属性の未使用
void print_vec_one_per_line(std::vector<int> &arr) {
for (const auto &x: arr) {
std::cout << x << '\n';
}
}
関数が参照先を変更しないことを明示するため、変更しないことが保証されている変数や引数にはconst
属性を追加した方が良い。
10. 文字列リテラルのライフタイムに対する誤解
const char *string_literal_lifetimes() {
return "string literals";
}
文字列リテラルの値はプロセス終了時まで保持されるため、上記の関数は問題なく動作する。
11. 構造化束縛の未使用
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';
}
C++17以降、構造化束縛を使用することにより、可読性を維持しつつメンバ変数に対する参照を複数まとめて初期化することができる。
for (const auto&[name, hex]: colors) {
std::cout << "name: " << name << ", hex: " << hex << '\n';
}
12. 出力用の参照渡しの使用
void get_values_out_params(const int n, int &out1, int &out2) {
// do stuff
out1 = n;
out2 = n + 1;
}
関数から複数の戻り値を返したい場合、参照渡しではなく専用の構造体を定義して戻り値として返した方が良い。
struct Values {
int x, y;
};
Values get_values_return_struct(const int n) {
return {n, n + 1};
}
戻り値の構造体は自動的にmoveされるので、実行時オーバーヘッドは発生しない。
13. コンパイル時処理できる演算に対する実行時処理4
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...
}
constexpr
属性を指定することで、関数(または変数)を定数式の内部で使用できるようになる。
実行時間短縮の他、テンプレート引数などの定数式が必要な箇所で関数を使用できるようになる。
14. virtual
属性が指定されていないデストラクタの使用
class BaseWithNonvirtualDestructor {
public:
void foo() {
std::cout << "do foo\n";
}
virtual ~BaseWithNonvirtualDestructor() {
std::cout << "called base destructor\n";
}
};
class Derived : public BaseWithNonvirtualDestructor {
public:
~Derived() override {
std::cout << "called derived destructor\n";
}
};
void consume_base(std::unique_ptr<BaseWithNonvirtualDestructor> p) {
p->foo();
// deletes p when done
}
void base_with_nonvirtual_destructor() {
auto derived = std::make_unique<Derived>();
consume_base(std::move(derived));
}
基底クラス型にキャストしたポインタからクラスを破棄しようとした場合、基底クラスのデストラクタにvirtual
属性がないと派生クラスのデストラクタが呼び出されなくなってしまい、クラスが正常に破棄されない可能性がある。
15. メンバ変数の初期化順に対する誤解
template<typename T>
class View {
public:
View(T *start, std::size_t size) : m_start{start}, m_end{m_start + size} {
std::cout << "view constructor: " << m_start << "," << m_end << '\n';
}
private:
T *m_start;
T *m_end;
};
void class_member_initialization_order() {
View<std::byte> v(nullptr, 1);
}
クラスのメンバ変数はコンストラクタ引数の順序ではなく、メンバの定義順に初期化される。
16. 未初期化の変数に対する参照
void default_vs_value_initialization() {
int x;
int *x2 = new int;
struct S {
int n, m;
std::string s;
S() {
std::cout << "constructor\n";
}
};
S my_s;
std::cout << my_s.n << " " << my_s.m << " \"" << my_s.s << "\"\n";
C、およびC++の変数はデフォルト初期化を実行した場合不定値を示すケースがあるので、初期値が必要な場合は明示的に初期化する必要がある。
原則として、変数は初期値を指定して定義した方が良い。
int y{};
int *y2 = new int{};
int *y3 = new int();
S my_s2{};
std::cout << my_s2.n << " " << my_s2.m << " \"" << my_s2.s << "\"\n";
}
17. マジックナンバーの過剰使用
float energy(float m) {
return m * 299792458.0 * 299792458.0;
}
参照先が定数の場合、コンパイラは自動的に最適化を行うので、数値リテラルはなるべく直接使用せずにconstexpr
変数などに格納した方が良い。
18. コンテナに対するイテレーションループ内での要素の追加・削除
void modify_while_iterating() {
std::vector<int> v{1, 2, 3, 4};
for (auto x : v) {
v.push_back(x);
}
for (auto it = v.begin(), end = v.end() ; it != end; ++it) {
v.push_back(*it);
}
std::copy(v.begin(), v.end(), std::back_inserter(v));
for (auto x: v) {
std::cout << x;
}
std::cout << '\n';
}
イテレーションの最中にコンテナの要素を追加・削除するとイテレータの指示先が変化する可能性があるため、注意を払う必要がある。
19. 関数の戻り値に対するstd::move
の使用
std::vector<int> make_vector(const int n) {
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> w{1, 2};
if (n == 0)
return std::move(v);
else
return std::move(w);
}
関数の戻り値は自動的に右辺値を返すように最適化される。
手動で指定した場合、場合によっては最適化が抑制されてしまうので、避けたほうが良い。
20. std::move
の挙動に対する誤解
以下はstd::move
の実装例である。
template<typename T>
constexpr std::remove_reference_t<T> &&
move(T &&value) noexcept {
return static_cast<std::remove_reference_t<T> &&>(value);
}
原則としてstd::move
は変数に対する参照を右辺値型にstatic_cast
するだけであり、メモリ間の移動は一切行わない。
21. 引数の評価順に依存した関数呼び出し
void g(int a, int b, int c) {
std::cout << a << ' ' << b << ' ' << c << '\n';
}
void function_evaluation_order_not_guaranteed() {
std::string s = "but i have heard it works even if you don't believe in it";
int i = 0;
const auto inc_i = [&i]() -> int {
return ++i;
};
g(inc_i(), inc_i(), inc_i());
}
C++において引数の評価順は規定されておらず、評価順によって結果が変わるコードの動作は未定義である。
22. 不必要なヒープアロケーションの実行
struct Record {
int id;
std::string name;
};
void unnecessary_heap_allocations() {
Record *customer = new Record{0, "James"};
Record *other = new Record{1, "Someone"};
// do work
delete customer;
delete other;
}
上記の例の場合、通常のスタックアロケーションで問題なく動作する。
new
による初期化を使用する前に、ヒープアロケーションが本当に必要かどうかもう一度確認した方が良い。
23. スマートポインタの未使用
struct Metadata {
std::byte magic;
std::size_t size;
std::array<char, 20> name;
std::array<char, 200> description;
// other fields
};
Metadata *read_metadata(const std::ifstream &file) {
auto *data = new Metadata{};
// read fields from file
return data;
}
変数をnew
を使用してアロケートした場合、例外処理などによりdelete
処理がスキップされた場合にメモリリークが発生してしまう。
あらかじめstd::unique_ptr
などのスマートポインタにアドレス値を格納しておくことで、オブジェクトの消滅時にメモリが自動解放される。
24. 生成用ヘルパー関数を使用しないスマートポインタの初期化
void f(std::unique_ptr<int> a, std::unique_ptr<float> b) {
}
void constructing_a_unique_ptr_directly() {
f(std::unique_ptr<int>(new int{}),
std::unique_ptr<float>(new float{}));
std::make_unique
及びstd::make_shared
関数を使用することにより、コンストラクタ引数をforwardしつつ、オブジェクトの動的確保を自動化することができる。
f(std::make_unique<int>(),
std::make_unique<float>());
25. コンテナ型定義以外でのnew
delete
構文の使用5
class Widget {
public:
Widget() : meta{new Metadata{}} {
// whatever
}
virtual ~Widget() {
delete meta;
}
private:
Metadata *meta;
};
メモリ管理に問題がない使用法であっても、クラス側で独自に管理するのは単一責任の原則から見てふさわしくないので、できるだけスマートポインタを使用したほうが望ましい。
class BetterWidget {
public:
BetterWidget() : meta{std::make_unique<Metadata>()} {
// whatever
}
private:
std::unique_ptr<Metadata> meta;
};
26. 手動リソース管理
void read_from_a_file(char *name) {
FILE *fp = fopen(name, "r");
// ... work with file, EXCEPTION?
fclose(fp);
new
及びdelete
と同様、例外処理などによりリソースの解放漏れが発生するおそれがあるので、自動的にリソース管理できるオブジェクトがあればそちらを使用した方が良い。
std::ifstream input{name};
// work with the file
このようなオブジェクトによる自動リソース管理をRAIIと呼び、例外安全なコードを書く為の推奨パターンとなっている。
ちなみに、std::unique_ptr
及びstd::shared_ptr
にテンプレート引数として関数を渡すことで、カスタムデリータを指定することができる。
27. スマートポインタの不必要な使用
std::shared_ptr<int> max(std::shared_ptr<int> a, std::shared_ptr<int> b) {
return *a > *b ? std::move(a) : std::move(b);
}
std::shared_ptr
の処理には実行時オーバーヘッドがあるため、引数などにおいて所有関係を管理する必要がない場合6はstd::shared_ptr
を使用せず生ポインタを使用した方が良い。
const int *max(const int *a, const int *b) {
return *a > *b ? a : b;
}
28. std::shared_ptr
の使い過ぎ
struct Pizza {
Pizza(float diameter, std::vector<std::string> toppings) :
m_diameter{diameter}, m_toppings{std::move(toppings)} {}
float m_diameter; // inches
std::vector<std::string> m_toppings;
};
std::shared_ptr<Pizza> make_shared_pepperoni_pizza(float diameter) {
std::vector<std::string> toppings = {"red sauce", "cheese", "pepperoni"};
return std::make_shared<Pizza>(diameter, std::move(toppings));
}
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);
std::shared_ptr<Pizza> shared_pizza2 = make_unique_pepperoni_pizza(16.0f);
}
前項と同様、参照カウンタが必要ない場合はstd::unique_ptr
を使用できないか検討した方が良い。
一時的に引数として利用するだけなら実体を参照渡しするかget
メソッドを使用すればよく、必要であればほぼノーコストでstd::shared_ptr
に変換できる。
29. スレッド安全性が要求される場面での不適切なstd::shared_ptr
の使用
struct Noisy {
int x;
Noisy() : x{0} {
}
~Noisy() {
std::osyncstream{std::cout} << "Deleted from thread " << std::this_thread::get_id() << '\n';
}
};
void worker(std::shared_ptr<Noisy> noisy) {
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 10000; ++j) {
noisy->x++;
}
std::osyncstream{std::cout} << "Thread " << std::this_thread::get_id() << " executing: " << noisy->x << "\n";
}
}
void shared_ptr_is_NOT_threadsafe() {
std::osyncstream{std::cout} << "Main thread " << std::this_thread::get_id() << " executing\n";
auto noisy = std::make_shared<Noisy>();
std::jthread t2(worker, noisy);
std::jthread t1(worker, noisy);
noisy.reset();
}
std::shared_ptr
の参照カウンタはスレッドセーフであるものの、アドレスの参照にはスレッド安全性が保証されていないため、スレッド安全性が必要な場合はstd::lock_guard
などで制御する必要がある。
30. const型ポインタとconst変数を指すポインタの混同
void const_pointer_vs_pointer_to_const() {
int x = 0;
int y = 0;
const int *ptr_to_const = &x;
int const *ptr_to_const2 = &x;
// *ptr_to_const = 1; // NOT ALLOWED
ptr_to_const = &y;
int *const const_ptr = &x;
*const_ptr = 1; // x == 1
// const_ptr = &y; // NOT ALLOWED
}
const int *
及びint const *
型はconst int
型に対する通常のポインタなので、ポインタそのものに対してはconst
属性が適用されない。const型のポインタを定義する場合はint * const
のように指定する必要がある。
31. (おまけ)コンパイラの警告無視
コード中に未定義動作が含まれている場合、コンパイラはwarningを生成するので、warningを無視すると重要な問題点を見逃してしまう恐れがある。
Python版もあるよ!
25 nooby Python habits you need to ditch - YouTube