blog

C++言語の目立たない15の機能

このリストは、C++言語のあらゆる側面を何年も何年も研究して集めた、C++言語の曖昧な特徴を集めたものです。C++は巨大で、私は常に新しいことを学んでいます。たとえあなたがC++を手の甲のように知って...

Jun 22, 2025 · 16 min. read
シェア

このリストは、C++言語の様々な側面を何年も何年も研究して集めた、C++言語の曖昧な機能のコレクションです。C++は巨大で、私は常に新しいことを学んでいます。たとえあなたがC++を手の甲のように知っていたとしても、このリストから何かを学んでいただければと思います。以下のリストは、最も無名なものから最も無名なものへとソートされています。

  • 角括弧の本当の意味
  • 最も厄介な構文解析
  • 算術マーカの代替
  • 再定義キーワード
  • Placement new
  • 変数宣言中の分岐
  • メンバ関数の参照修飾子
  • 完全なテンプレートメタプログラミングへの移行
  • メンバーへのポインタ演算子
  • 静的インスタンスメソッド
  • ++
  • オペレータのオーバーロードとチェック順序
  • 関数をテンプレートパラメータ
  • テンプレートのパラメータもテンプレート
  • try関数としてのブロック

角括弧の本当の意味

配列要素へのアクセスに使われるptr[3]は、実際には*の省略形でしかなく、これは*を使うのと等価であり、したがって3[ptr]と等価であり、3[ptr]を使うことは完全に有効なコードです。

最も厄介な解析

最も厄介な構文解析(most vexing parse)」という言葉は、スコット・マイヤーズ(Scott Meyers)氏による造語で、C++の構文宣言の二重性が直感に反する動作を引き起こすことがあるためです:

// この説明は正しいだろうか? 
// 1) std型の::string関数の変数はstd::string()インスタンス化? 
// 2) 関数宣言がstd::stringこの構文は、コンストラクタに限らず、他のすべての関数定義にも使える。, 
// また、この関数はstd::stringしかし、パラメータはないのか? 
std::string foo(std::string()); 
  
// それとも、これは正しいのだろうか? 
// 1)int型変数はintでインスタンス化されるのか? 
// 2)引数1つでint値を返す関数宣言, 
// 引数はxという名前のint変数か? 
int bar(int(x)); 

C++の標準では、どちらの場合も2番目の解釈が要求されています。プログラマは、変数の初期値を括弧で囲むことで、曖昧さを取り除くことができます:

//括弧を追加することで曖昧さを取り除く 
std::string foo((std::string())); 
int bar((int(x))); 

なぜなら、int y = 3; は int(y) = 3; と等価だからです。

#include <iostream> 
#include <string> 
using namespace std; 
  
int bar(int(x));   // 等价于int bar(int x) 
  
string foo(string());  // 等价于string foo(string (*)()) 
  
string test() { 
    return "test"; 
} 
  
int main() 
{ 
    cout << bar(2) << endl; // 出力2 
    cout << foo(test); // 出力テスト 
    return 0; 
} 
  
int bar(int(x)) {  
    return x; 
} 
  
string foo(string (*fun)()) { 
    return (*fun)(); 
} 

正しく出力されますが、作者の意図通りに括弧を追加してコンパイルすると、「このスコープでは宣言されていません」、「再定義されています」など、作者が何を意図したのかわからないエラーが大量に発生します。

代入演算子

and、and_eq、bitand、bitor、compl、not、not_eq、or、or_eq、xor、xor_eq、<%、%>、<:、:>は、通常の&&、&=、&、 |、~、 !, !=, , |=, ^, ^=, {, }, [ and ].必要な記号がキーボードにない場合、これらの算術記号を代わりに使うことができます。

キーワードの再定義

プリプロセッサによるキーワードの再定義は、技術的にはエラーを引き起こしますが、実際には許可されています。そのため、#define true falseや#define elseのようなものを使っていたずらをすることができます。例えば、大規模なライブラリを扱っていて、C++のアクセス保護機構をバイパスする必要がある場合、ライブラリにパッチを当てるだけでなく、ライブラリヘッダをインクルードするときにアクセス保護をオフにすることもできますが、ライブラリヘッダをインクルードした後に保護をオンにすることを忘れないでください!

#define class struct 
#define private public 
#define protected public 
  
#include "library.h" 
  
#undef class 
#undef private 
#undef protected 

これはコンパイラに依存するため、毎回うまくいくわけではないことに注意してください。インスタンス変数がアクセス制御によって変更されない場合、C++ は単に順番に並べるだけなので、コンパイラはアクセス制御のセットを並べ替えることによってメモリレイアウトを自由に変更できます。たとえば、コンパイラはすべてのプライベート メンバをパブリック メンバの後に移動させることができます。マイクロソフト社のC++コンパイラは、アクセス制御の一致を名前マングリングテーブルにマージするため、アクセス制御文字を変更すると、既存のコンパイル済みコードとの互換性が失われます。

#p#

プレースメント

Placement new は new 演算子に代わる構文で、正しくサイズ指定され、正しく代入されたオブジェクトに作用します。

#include <iostream> 
using namespace std; 
  
struct Test { 
  int data; 
  Test() { cout << "Test::Test()" << endl; } 
  ~Test() { cout << "Test::~Test()" << endl; } 
}; 
  
int main() { 
  // Must allocate our own memory 
  Test *ptr = (Test *)malloc(sizeof(Test)); 
  
  // Use placement new 
  new (ptr) Test; 
  
  // Must call the destructor ourselves 
  ptr->~Test(); 
  
  // Must release the memory ourselves 
  free(ptr); 
  
  return 0; 
} 

例えば、スラブ・アロケータは、単一の大きなメモリ・ブロックから開始し、placement new を使ってブロック内のオブジェクトを順番に割り当てます。これはメモリの断片化を避けるだけでなく、mallocによるヒープトラバーサルオーバーヘッドを節約します。

変数宣言中の分岐

C++には、変数の宣言中に分岐を可能にする構文の省略形があります。単一の変数宣言のようにも見えますし、ifやwhileのような分岐条件を持つこともできます。

struct Event { virtual ~Event() {} }; 
struct MouseEvent : Event { int x, y; }; 
struct KeyboardEvent : Event { int key; }; 
  
void log(Event *event) { 
  if (MouseEvent *mouse = dynamic_cast<MouseEvent *>(event)) 
    std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl; 
  
  else if (KeyboardEvent *keyboard = dynamic_cast<KeyboardEvent *>(event)) 
    std::cout << "KeyboardEvent " << keyboard->key << std::endl; 
  
  else 
    std::cout << "Event" << std::endl; 
} 

メンバ関数の参照修飾子

C++11 では、メンバ関数がオブジェクトの値型に対してオーバーロードできるようになっており、thisポインタはオブジェクトを参照修飾子として扱います。参照修飾子は cv-qualifier と同じ場所に配置され、this オブジェクトが左値か右値かに応じてオーバーロードの解決に影響します:

#include <iostream> 
  
struct Foo { 
  void foo() & { std::cout << "lvalue" << std::endl; } 
  void foo() && { std::cout << "rvalue" << std::endl; } 
}; 
  
int main() { 
  Foo foo; 
  foo.foo(); // Prints "lvalue" 
  Foo().foo(); // Prints "rvalue" 
  return 0; 
} 

完全なテンプレートメタプログラミングへの移行

C++テンプレートは、コンパイル時のメタプログラミング、つまりプログラムが他のプログラムを生成する機能を実現するためのものです。テンプレート・システムはもともと単純な型置換を実行するように設計されていましたが、C++の標準化の過程で、テンプレートが実際には任意の計算を実行するのに十分強力であることが突然明らかになりました:

// Recursive template for general case 
template <int N> 
struct factorial { 
  enum { value = N * factorial<N - 1>::value }; 
}; 
  
// Template specialization for base case 
template <> 
struct factorial<0> { 
  enum { value = 1 }; 
}; 
  
enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120 

C++テンプレートは、反復ではなく再帰を使用し、不変な状態を含むため、関数型プログラミング言語とみなすことができます。typedef を使用して任意の型の変数を作成でき、enum を使用して int 型の変数を作成できます。

// Compile-time list of integers 
template <int D, typename N> 
struct node { 
  enum { data = D }; 
  typedef N next; 
}; 
struct end {}; 
  
// Compile-time sum function 
template <typename L> 
struct sum { 
  enum { value = L::data + sum<typename L::next>::value }; 
}; 
template <> 
struct sum<end> { 
  enum { value = 0 }; 
}; 
  
// Data structures are embedded in types 
typedef node<1, node<2, node<3, end> > > list123; 
enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6 

もちろん、これらの例はあまり役に立ちませんが、テンプレート・メタプログラミングでは、型のリストを操作できるなど、便利なこともできます。しかし、C++のテンプレートを使用するプログラミング言語の使い勝手は極めて低いので、使用は慎重に、かつ少量にしましょう。テンプレート・コードは読みにくく、コンパイルが遅く、エラー・メッセージが長くてわかりにくいためデバッグが困難です。

#p#

メンバーへのポインタ演算子

pointer-to-member 演算子を使うと、クラスのインスタンス上のメンバへのポインタを記述できます。pointer-to-member 演算子には、fetch 演算子 * と pointer 演算子 -> の 2 種類があります:

#include <iostream> 
using namespace std; 
  
struct Test { 
  int num; 
  void func() {} 
}; 
  
// Notice the extra "Test::" in the pointer type 
int Test::*ptr_num = &Test::num; 
void (Test::*ptr_func)() = &Test::func; 
  
int main() { 
  Test t; 
  Test *pt = new Test; 
  
  // Call the stored member function 
  (t.*ptr_func)(); 
  (pt->*ptr_func)(); 
  
  // Set the variable in the stored member slot 
  t.*ptr_num = 1; 
  pt->*ptr_num = 2; 
  
  delete pt; 
  return 0; 
} 

この機能は、特にライブラリを書くときに非常に便利です。例えば、C++をPythonのオブジェクトにバインドするためのライブラリであるBoost::Pythonでは、オブジェクトをラップする際にメンバを簡単に指定するためにメンバポインタ演算子を使っています。

#include <iostream> 
#include <boost/python.hpp> 
using namespace boost::python; 
  
struct World { 
  std::string msg; 
  void greet() { std::cout << msg << std::endl; } 
}; 
  
BOOST_PYTHON_MODULE(hello) { 
  class_<World>("World") 
    .def_readwrite("msg", &World::msg) 
    .def("greet", &World::greet); 
} 

メンバ関数ポインタの使用は、通常の関数ポインタとは異なることを覚えておいてください。メンバ関数ポインタと通常の関数ポインタ間のキャストは無効です。例えば、Microsoftコンパイラのメンバ関数は thisコールと呼ばれる最適化された呼び出し方をします。this "コールは thisパラメータをecxレジスタに置きますが、通常の関数の呼び出し規約はすべてのパラメータをスタックに解決します。

さらに、メンバ関数ポインタは通常のポインタの約4倍の大きさになることがあり、コンパイラは関数本体のアドレス、正しい親アドレスへのオフセット、仮想関数テーブル内の別のオフセットのインデックス、さらには仮想関数テーブル内のオフセットをオブジェクト自体に格納する必要があります。

#include <iostream> 
  
struct A {}; 
struct B : virtual A {}; 
struct C {}; 
struct D : A, C {}; 
struct E; 
  
int main() { 
  std::cout << sizeof(void (A::*)()) << std::endl; 
  std::cout << sizeof(void (B::*)()) << std::endl; 
  std::cout << sizeof(void (D::*)()) << std::endl; 
  std::cout << sizeof(void (E::*)()) << std::endl; 
  return 0; 
} 
  
// 32-bit Visual C++ 2008:  A = 4, B = 8, D = 12, E = 16 
// 32-bit GCC 4.2.1:        A = 8, B = 8, D = 8,  E = 8 
// 32-bit Digital Mars C++: A = 4, B = 4, D = 4,  E = 4 

Digital Marsコンパイラのメンバ関数はすべて同じサイズですが、これは、ポインタ自体にオフセットを格納するのではなく、右オフセットを適用する「サンク」関数を生成する巧妙な設計によるものです。

静的インスタンスメソッド

C++ の静的メソッドは、インスタンスまたはクラスから直接呼び出すことができます。これにより、呼び出しポイントを更新することなく、インスタンス・メソッドを静的メソッドに変更できます。

struct Foo { 
  static void foo() {} 
}; 
  
// These are equivalent 
Foo::foo(); 
Foo().foo(); 

オーバーロード ++ と -

C++ では、カスタム演算子の関数名が演算子そのものになるように設計されています。たとえば、単項演算子の - は、引数の数によって二項演算子の - と区別できます。しかし、単項演算子のインクリメント演算子とデクリメント演算子では、同じ特性を持つように見えますが、これは機能しません。c++には、この問題を回避するための厄介なトリックがあります。

struct Number { 
  Number &operator ++ (); // Generate a prefix ++ operator 
  Number operator ++ (int); // Generate a postfix ++ operator 
}; 

オペレータのオーバーロードとチェック順序

や&&演算子をオーバーロードすると、通常のチェックルールが崩れるので混乱することがあります。通常、カンマ演算子は左辺全体をチェックしてから右辺のチェックを開始し、&&演算子は短絡的な動作をします。いずれにせよ、演算子のオーバーロード版は単なる関数呼び出しであり、関数呼び出しは不特定の順序で引数をチェックします。

これらの演算子をオーバーロードすることは、C++の構文を悪用する方法にすぎません。例として、括弧のないバージョンのprint文のPython形式のC++実装を以下に示します:

#include <iostream> 
  
namespace __hidden__ { 
  struct print { 
    bool space; 
    print() : space(false) {} 
    ~print() { std::cout << std::endl; } 
  
    template <typename T> 
    print &operator , (const T &t) { 
      if (space) std::cout << ' '; 
      else space = true; 
      std::cout << t; 
      return *this; 
    } 
  }; 
} 
  
#define print __hidden__::print(), 
  
int main() { 
  int a = 1, b = 2; 
  print "this is a test"; 
  print "the sum of", a, "and", b, "is", a + b; 
  return 0; 
} 

#p#

関数をテンプレートパラメータ

テンプレート・パラメーターが特定の整数か特定の関数であることはよく知られています。これにより、コンパイラはテンプレート・コードをインスタンス化する際に、特定の関数をインラインで呼び出すことができ、より効率的な実行が可能になります。以下の例では、memoize 関数のテンプレート引数も関数であり、新しい引数値のみが関数を通して呼び出されます:

#include <map> 
  
template <int (*f)(int)> 
int memoize(int x) { 
  static std::map<int, int> cache; 
  std::map<int, int>::iterator y = cache.find(x); 
  if (y != cache.end()) return y->second; 
  return cache[x] = f(x); 
} 
  
int fib(int n) { 
  if (n < 2) return n; 
  return memoize<fib>(n - 1) + memoize<fib>(n - 2); 
} 

テンプレートのパラメータもテンプレート

テンプレート・パラメータは、実際にはテンプレートそのものにすることができます。これにより、テンプレートをインスタンス化するときに、テンプレート・パラメータなしでテンプレート・タイプを渡すことができます。以下のコードを見てください:

template <typename T> 
struct Cache { ... }; 
  
template <typename T> 
struct NetworkStore { ... }; 
  
template <typename T> 
struct MemoryStore { ... }; 
  
template <typename Store, typename T> 
struct CachedStore { 
  Store store; 
  Cache<T> cache; 
}; 
  
CachedStore<NetworkStore<int>, int> a; 
CachedStore<MemoryStore<int>, int> b; 

CachedStore のキャッシュには、ストアと同じデータ型が保存されます。しかし、CachedStore をインスタンス化する際には、データ型をストア自身と CachedStore の両方で繰り返し記述する必要があります。データ型を決定したいのは本当に一度だけなので、強制的にデータ型を変更しないようにすることができますが、型パラメータのないリストはコンパイルエラーになります:

// 以下は、NetworkStoreとMemoryStoreの型パラメーターがないため、コンパイルできない。 
CachedStore<NetworkStore, int> c; 
CachedStore<MemoryStore, int> d; 

テンプレートのテンプレート・パラメータを使用すると、必要な構文を得ることができます。テンプレート・パラメータとして class キーワードを使用しなければならないことに注意してください。

template <template <typename> class Store, typename T> 
struct CachedStore2 { 
  Store<T> store; 
  Cache<T> cache; 
}; 
  
CachedStore2<NetworkStore, int> e; 
CachedStore2<MemoryStore, int> f; 

関数としてのtryブロック

関数の try ブロックは、コンストラクタの初期化リストのチェック中にスローされた例外をキャッチします。try-catchブロックは関数本体の外側にしか表示できないため、初期化リストを囲むようにtry-catchブロックを配置することはできません。この問題を解決するために、C++ では try-catch ブロックを関数本体としても使用できるようにしています:

int f() { throw 0; } 
  
// ここでf()がスローした例外をキャッチする方法はない。 
struct A { 
  int a; 
  A::A() : a(f()) {} 
}; 
  
// try-catchブロックが関数本体として使用され、初期化リストがtryキーワードの後に移動した場合、次のようになる。, 
// 不思議なことに、この構文はコンストラクタに限らず、他のすべての関数定義にも使える。 
struct B { 
  int b; 
  B::B() try : b(f()) { 
  } catch(int e) { 
  } 
}; 

不思議なことに、この構文はコンストラクタに限らず、他のすべての関数定義にも使えます。

Read next