江添亮のC++入門 を読んで見る

7月 13, 2020

C++の経験が20年以上あるのですが、改めて入門という教材を見たら、何か新しい発見があるのではと思い、調べて見ました。
https://ezoeryou.github.io/cpp-intro/

この解説書は、とても内容量が多く、一通り読み終わるのに随分時間を費やしました


教材の実行環境

Linux環境でのプログラム実行例が示されています。私は、Windows 10 で次のツールを使い試しました。

  • エディター Visual Studio Code
  • gcc MinGW http://www.mingw.org/
  • clang https://clang.llvm.org/
  • make http://gnuwin32.sourceforge.net/packages/make.htm

再起関数

この節の例で、「1, 0のみを使った10進数から2進数へ変換する関数」があるが、プログラムの例は、「2進数から10進数へ変換する関数」に思えます。私が何か勘違いしている?

整数型のサイズ

例はLinux の例が出ています。Windows 10では、下記の通りです。昔調べた時から変わっていないですね。

auto print = [](std::size_t s)
{ std::cout << s << "\n"s; };

print(sizeof(char));
print(sizeof(short));
print(sizeof(int));
print(sizeof(long));
print(sizeof(long long));
1
2
4
4
8

整数型の表現できる値の範囲

INI_MIN や INT_MAX は使っていましたが、std にあることは知らなかった…。何が良い?

std::cout
    << std::numeric_limits<int>::min() << "\n"s
    << std::numeric_limits<int>::max() << "\n"s
    << std::numeric_limits<unsigned int>::min() << "\n"s
    << std::numeric_limits<unsigned int>::max() << "\n"s;
-2147483648
2147483647
0
4294967295

NaN (Not a Number)

使うことはないと思いますが、a == b ならば a != b が成り立たないケース。

    double NaN = std::numeric_limits<double>::quiet_NaN() ;

    // true
    bool b = NaN != 0.0 ;

    // false
    bool a = NaN == 0.0 ;
    bool c = NaN == NaN ;
    bool d = NaN != NaN ;
    bool e = NaN < 0.0 ;

有効桁数

std::cout
    << "float: "s << std::numeric_limits<float>::digits << "\n"s
    << "double: "s << std::numeric_limits<double>::digits << "\n"s
    << "long double: "s << std::numeric_limits<long double>::digits << "\n"s

    << "float: "s << std::numeric_limits<float>::digits10 << "\n"s
    << "double: "s << std::numeric_limits<double>::digits10 << "\n"s
    << "long double: "s << std::numeric_limits<long double>::digits10 << "\n"s
            
    << "float: "s << std::numeric_limits<float>::max_digits10 << "\n"s
    << "double: "s << std::numeric_limits<double>::max_digits10 << "\n"s
    << "long double: "s << std::numeric_limits<long double>::max_digits10 << "\n"s;   
float: 24
double: 53
long double: 64
float: 6
double: 15
long double: 18
float: 9
double: 17
long double: 21

浮動小数点の1と比較可能な最小な値との差

std::cout
    << "float: "s << std::numeric_limits<float>::epsilon() << "\n"s
    << "double: "s << std::numeric_limits<double>::epsilon() << "\n"s
    << "long double: "s << std::numeric_limits<long double>::epsilon() << "\n"s ;
float: 1.19209e-007
double: 2.22045e-016
long double: 1.0842e-019

const

const int x = 0;
int const y = 0;

1番目しか書いたことない…。

int a = 0;
const int &b = a; // ok
int const &c = a; // ok
int & const d = a; // error

組み込み型の初期化

int a = 0;
int a(0);
int a{0};

1番目しか書いたことない…。3番目は、structで使うことはありますが。

std::vectorstd::array の処理時間差

動的配列 vector と固定長配列 array の処理時間を比較してみました。

vector は、array と比較してメモリ拡張のために倍の時間が掛かっています。ただし、宣言時に必要な数を一度に確保してしまえば、array とほぼ変わらなくなります。

vector at() による参照は、[] の倍の処理時間が掛かっています。at() は配列外参照のチェックがあるからでしょうか?

        using namespace std;

        chrono::system_clock::time_point start, end;
        const int num = 100'000;
        std::vector<int> v1;
        std::array<int, num> ar, ar2;

        auto print_time = [](string s, chrono::system_clock::time_point start, chrono::system_clock::time_point end)
        {
            double time = static_cast<double>(chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0);
            cout << s << " = " << time << endl;
        };

        start = chrono::system_clock::now();
        for (int ic = 0; ic < num; ic++) {
            v1.push_back(ic);
        }
        end = chrono::system_clock::now();
        print_time("vector push_back", start, end);

        start = chrono::system_clock::now();
        for (int ic = 0; ic < num; ic++) {
            ar[ic] = ic;
        }
        end = chrono::system_clock::now();
        print_time("array '='", start, end);


        start = chrono::system_clock::now();
        for (int ic = 0; ic < num; ic++) {
            ar2[ic] = v1[ic];
        }
        end = chrono::system_clock::now();
        print_time("vector []", start, end);

        start = chrono::system_clock::now();
        for (int ic = 0; ic < num; ic++) {
            ar2[ic] = v1.at(ic);
        }
        end = chrono::system_clock::now();
        print_time("vector at()", start, end);

        start = chrono::system_clock::now();
        for (int ic = 0; ic < num; ic++) {
            ar2[ic] = ar[ic];
        }
        end = chrono::system_clock::now();
        print_time("array '='", start, end);
vector push_back = 1.998
array '=' = 1.031
vector [] = 0.997
vector at() = 1.994
array '=' = 1.995

std::basic_string_view

basic_string_viewはストレージを所有しないクラス

basic_string_viewは文字列がnull終端文字列とbasic_stringのどちらで表現されていても問題なく受け取るためのクラス

namespace std {
    template <
        typename charT,
        typename traits = char_traits<charT>
    >
    class basic_string_view ;
}

basic_string_viewにはbasic_stringと対になる各文字型に対する特殊化がある。

namespace std {
    using string_view    = basic_string_view<char> ;
    using u8string_view  = basic_string_view<char8_t> ;
    using u16string_view = basic_string_view<char16_t> ;
    using u32string_view = basic_string_view<char32_t> ;  
    using wstring_view   = basic_string_view<wchar_t> ;
}

各basic_stringに対するユーザー定義リテラルサフィックスsvがある。

// string_view
auto str    = "hello"sv ;
// u8string_view
auto u8str  = u8"hello"sv ;
// u16string_view
auto u16str = u"hello"sv ;
// u32string_view
auto u32str = U"hello"sv ;
// wstring_view
auto wstr   = L"hello"sv ;

二項分布(std::binomial_distribution)
指数分布(std::exponential_distribution)
正規分布(std::normal_distribution)

VA_OPT

VA_OPTは可変長引数マクロでVA_ARGSにトークン列が渡されたかどうかで置換結果を変えることができる。

VA_OPTは可変引数マクロの置換リストでのみ使える。VA_OPT(content)はVA_ARGSにトークンがない場合はトークンなしに置換され、トークンがある場合はトークン列contentに置換される。

#演算子

#はマクロ実引数を文字列リテラルにする。

#は関数風マクロの置換リストの中のみで使うことができる。#は関数風マクロの仮引数の識別子の直前に書くことができる。#が直前に書かれた識別子は、マクロ実引数のトークン列の文字列リテラルになる。

##演算子

##はマクロ実引数の結合を行う。

##は関数風マクロの置換リストの中にしか書けない。##は両端にマクロの仮引数の識別子を書かなければならない。##は両端の識別子の参照するマクロ実引数のトークン列を結合した置換を行う。