C++新しく学んだこと

Parameter pack

0個以上の可変長の複数の変数をまとめて扱い展開していく時に使う。

Reference

Parameter pack(since C++11) - cppreference.com

Example1

template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) {
  for (; *format != '0\n'; format++) {
    if (*format == '%') {
      tprintf(format+1, Fargs);
      return;
    }
  }
}

int main()
{
  tprintf("% world% %\n","Hello",'!',123);
}

出力

Hello world! 123

動作イメージ

parameter packの動作イメージ

コメント

型としては必ずテンプレートを使わないといけない。例えば以下のような使い方はできない。

void value_print(int value, int... values) {
  std::cout << value << std::endl;
  value_print(values);
}

Example2

デバッグモードの時のみ値をprintする関数

/// @brief デバッグモードの時のみ値をprintする関数
template <typename... Targs>
void print(Targs... args) {
# if DEBUG
  printf(args...); // 引数に...をつけることに注意
# endif
  return; 
}

テンプレートと非テンプレートで関数の引数につけられた&&の挙動の違い

非テンプレートの場合、&&がついた引数は右辺値しか受け取ることができない。一方テンプレートの場合は&&がついた引数は右辺値のみではなく左辺値も受け取ることができる。

class Hoge {
};
template <typename T>
void templae_func(T && h){
  (void)h;
}

void nontemplae_func(Hoge && h){
  (void)h;
}
int main() {
  Hoge h;
  templae_func(h);
  nontemplae_func(h); // これはエラーが
}

Reference

std::forwardについて

std::forward

上で書いたように、テンプレート関数の引数に&&がついていると、その引数は右辺値と左辺値の両方の値がくる可能性がある。ただ、それをそのまま別の関数に渡してしまうと仮に引数が右辺値型で来ていたとしても右辺値を受け取った引数は左辺値として扱われるため別の関数に左辺値として渡ってしまうという問題がある。例えば以下のようにすると右辺値引数の関数func2を呼べずエラーになる。

class Hoge {
};
void func2(Hoge && h) {
  std::cout << "func2 double and" << std::endl;
}
template <typename T>
void func(T && h){
  func2(h);
}
int main() {
  func(Hoge());
}

そこでstd::forwardの出番である。これは左辺値で来たものを左辺値のまま、右辺値で来たものを右辺値のままにキャストするものであり、func内でこれを使うとこれがエラーではなくなる。

class Hoge {
};
void func2(Hoge && h) {
  std::cout << "func2 double and" << std::endl;
}
template <typename T>
void func(T && h){
  func2(std::forward<T>(h));
}
int main() {
  func(Hoge());
}

マクロの可変長引数展開

やりたいこと

MACRO(3, 4, 5);

とやったらある処理を3、4、5の3ケースについてループしてくれるようなことや

MACRO(add, subt, multiple);

とやったらある処理をaddsubtmultipleの3関数についてループしてくれるようなこと。

マクロループ展開のやり方

マクロループは(a)(b)(c)のようなマクロ形式の集合の形で渡す必要がある。例えば

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/variadic/to_seq.hpp>

// dataの取り扱いは不明
#define MACRO(r, data, i, elem) std::cout << "r="<<r<<",i="<<i <<",elem="<<elem<< std::endl;
// 第二引数は使わなくてよさようだ
#define MACRO2() BOOST_PP_SEQ_FOR_EACH_I(MACRO, _, (3)(4)(5)); 
int main() {
  MACRO2();
  return 0;
}

とやると

r=1,i=0,elem=3
r=2,i=1,elem=4
r=3,i=2,elem=5

が出力される。

マクロ形式の集合の形の作り方

BOOST_PP_VARIADIC_TO_SEQを使うと (a, b, c)と渡した引数を(a)(b)(c)と変換できる。またマクロの引数を...としそのマクロ内で__VA_ARGS__を使うと可変長引数を受けられる。例えば

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/variadic/to_seq.hpp>
#define MACRO(r, data, i, elem) std::cout << "r="<<r<<",i="<<i <<",elem="<<elem<< std::endl;
#define MACRO2(...) BOOST_PP_SEQ_FOR_EACH_I(MACRO, _, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__));
int main() {
  MACRO2("3", "4", "5");
  return 0;
}

とやると

r=1,i=0,elem=3
r=2,i=1,elem=4
r=3,i=2,elem=5

と出力される。

マクロに関数を渡す。

マクロには数値や文字だけでなく関数も渡すことができる。

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/variadic/to_seq.hpp>
int add(int i) {
  return i+1;
}
int subt(int i) {
  return i-1;
}
int multiple(int i) {
  return i*2;
}
#define MACRO(r, data, i, elem) std::cout << "i="<<i<<",elem="<<elem(3) << std::endl;
#define MACRO2(...) BOOST_PP_SEQ_FOR_EACH_I(MACRO, _, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__));
int main() {
  MACRO2(add, subt, multiple)
  return 0;
}

出力結果は

i=0,elem=4
i=1,elem=2
i=2,elem=6

headerの引数にconstをつけない

// hpp
void f(int v);

// cpp
void f(const int v) {
}

のように、値で渡す引数については、headerの方はconstをつけずcppの方にはconstをつける。

ポリモーフィズムの基底クラスのデストラクタにはvirtualをつける

ポリモーフィズムとは

int main() {
  Base *b = new   Derived();
  delete b;
  return 0;
}

のように、Base型のポインタの変数にDerived型のポインタを割り当てることである。このようなポリモーフィズムの使い方をされる場合は、基底クラスのデストラクタにvirtualをつけないといけない。

virtualをつけた場合

class Base {
public:
  Base() = default;
  virtual ~Base() = default;
};

class Derived :    public Base {
public:
  Derived() { std::cout<<"Derived constructor called" << std::endl;}
  ~Derived() { std::cout<<"Derived destructor called" << std::endl;}
};

出力は

Derived constructor called
Derived destructor called

virtualをつけなかった場合

class Base {
public:
  Base() = default;
  ~Base() = default;                                                                                                       
};

class Derived :    public Base {
public:
  Derived() { std::cout<<"Derived constructor called" << std::endl;}
  ~Derived() { std::cout<<"Derived destructor called" << std::endl;}
};

出力は

Derived constructor called

となりデストラクタが呼ばれなくなってしまう。

無理やりキャスト

普通は違うクラスへのポインタを変換できない

Class1* p1 = new Class1();
Class2 * p2 =   p1;

以下のようなエラーが出る。

error: cannot initialize a variable of type 'Class2 *' with an lvalue of type 'Class1 *'

reinterpret_castを使うと無理やりキャストすることができる。

Class1* p1 = new Class1();
Class2* p2 = reinterpret_cast<Class2*>(p1);

矢印を使った関数の定義のしかた

void b(int v) {
  (void)v;
}

と全く同じ関数定義を

auto b(int v) -> void {
  (void)v;
}

のような書き方で記述することができる。これが使われるケースは、引数の型によって返り値の型を変更したい時。

例えば

#include <iostream>

template <typename T>
auto func(T v) -> decltype(v) {
  return v;
}

int main() {
  std::cout << func(3) << std::endl;
  std::cout << func(4.2) << std::endl;
  return 0;
}

のようなことができる。 通常のように

decltype(v) func(T v) {
  ...
}

と記載してしまうとvの定義の前にdecltype(v)が呼ばれてしまいエラーとなってしまう。

参考

c++ - Arrow operator (->) in function heading - Stack Overflow

条件に応じた型設定

条件に応じて変数の型を変えたい場合はstd::conditionalを使えば良い。

#include <iostream>

int main() {
  std::conditional<true, int, double>::type a = 4.2;
  std::conditional<false, int, double>::type b = 4.2;
  std::cout << "a=" << a << std::endl;
  std::cout << "b=" << b << std::endl;
  return 0;
}

とすると、aint型、bdouble型として定義される。

出力は

a=4
b=4.2

具体的な条件を設定してみると

#include <iostream>

// 引数がint型ならint型の値を出力し、それ以外ならdouble型の値を出力する。
template <typename T>
void func(const T v) {
  typename std::conditional<std::is_same<T, int>::value, int, double>::type c = 4.2;
  std::cout << c << std::endl;
}

int main() {
  func(3);
  func(2.4);
  return 0;
}

ポイントとしては、std::conditionalの前にtypenameをつけないとエラーになる。

出力は

4
4.2

となる。

条件に応じてテンプレート関数を使い分ける。

decltypeを使ったやり方

#include <iostream>
#include <vector>

// 引数がbegin()を持つデータ構造の時だけ定義される関数                                                                       
template <typename T>
auto func(const T v) -> decltype(v.begin(), void()){
  std::cout << "begin() defined" << std::endl;
}

// 引数が+1の演算が定義できるデータ構造の時だけ定義される関数                                                                
template <typename T>
auto func(const T v) -> decltype(v + 1, void()){
  std::cout << "+1 defined" << std::endl;
}

// こういう関数を定義してもエラーにならない                                                                         
template <typename T>
auto func(const T v) -> decltype(v.bbbegin(), void()){
  std::cout << "+1 defined" << std::endl;
}

int main() {
  func(3);
  std::vector<int > a{3, 4, 5};
  func(a);
  //func({3, 4, 5}); // これはエラー                                                                                   
  return 0;
}

ポイントはdecltype(v + 1, void())の部分で、,演算子としてみなされ、,の左側の返り値は無視されて,の右側の返り値が代入される。またvoidなどの型に()をつける必要があるようだ。すなわち、decltype(v + 1, void())の部分は 「v+1が定義されているのであればvoid型」となる。

出力は

+1 defined
begin() defined

enable_ifを使ったやり方

decltypeを使うと少し冗長な記述になるので、enable_ifを使った記述方法についてまとめた。

#include <iostream>
#include <vector>
#include <type_traits>

// fundamental型の引数のときのみ使えるテンプレート関数                                                           
template <typename T>
typename std::enable_if<std::is_fundamental<T>::value, void>::type func(const T v) {
  std::cout << "fundamental" << std::endl;
}

int main() {
  func(3);
  // func("hoge"); // 未定義エラー                                                                                           
  return 0;
}

ここで、C++14以降であればenable_if_tを使うことができる。これはcppreference.comを読むと

template< bool B, class T = void >
using enable_if_t = typename enable_if<B,T>::type;

とあり、defaultでvoidの型が選択されるようだ。

// C++14以降の別の書き方                                                                                             
template <typename T>
typename std::enable_if_t<std::is_fundamental<T>::value> func(const T v) {
  std::cout << "fundamental" << std::endl;
}

また、fundamental型以外の型が引数として与えられた時に呼ばれる関数を定義したい時はfunc(...)とすれば良い。すなわち

#include <iostream>
#include <vector>
#include <type_traits>

// fundamental型の引数のときのみ使えるテンプレート関数                                                           
template <typename T>
typename std::enable_if<std::is_fundamental<T>::value, void>::type func(const T v) {
  std::cout << "fundamental" << std::endl;
}

void func(...) {
  std::cout << "others"    << std::endl;
}

int main() {
  func(3);
  func("hoge");
  return 0;
}

とすると

fundamental
others

と出力される。

インライン

関数の呼び出しにはコストがかかるので、何度も呼ばれる短い関数はインラインにすると定義をそのまま呼び出し元にコピーするので高速化できることがある。
インラインの仕方は、cppとhppのように定義と実装を分けず、hppに定義実装をしてinlineをつけること。ただしこのように定義してもコンパイラがインラインにすることは保証されない。

As-if ルール

プログラムの観測結果を変えない限りはどんなコード変換も許されるというルール。 copy/move elisionのときだけ例外がある。

The as-if rule - cppreference.com