LoginSignup
63
53

C++で実装するなら(2023年時点での考え)

Last updated at Posted at 2023-08-04

最近、C++を書いていない。
Pythonで書いていることが多いからだ。
C++を書けるということをどういう意味で期待していますか?

C言語のレガシーがコードの安全性を損なっている。

  • C++言語はC言語の拡張という枠組みになっています。そのため、C言語ヘッダファイルをinclude することができてしまいます。
  • C言語では、ヘッダファイルのincludeする順序で動作が変わってしまうこと
  • ヘッダファイル中の書かれているdefine マクロが、あなたの書いたソースコードを書き換えてしまうのです。
  • 有名なところではwindows.h のmin() max() のマクロが多くのコードと相容れないという問題を持っています。
    https://yohhoy.hatenadiary.jp/entry/20120115/p1
  • C言語のコードは、C++言語としても有効なコードになっているので、同じ弱点がC++言語にも共通してしまっているのです。

いまの時点でのC++が書けるということを意味するには

  • なるべく最新の仕様にしたがった安全なC++の使い方でのC++の書き方をできること。
    • maloc() free()などのC言語由来のメモリ管理を使わない。
  • 2020年代の時点で、使っていいライブラリと使うべきではないライブラリを区別できていること。
  • 2020年代の時点で推奨されるコーディングスタイルにそって、コードとドキュメンテーションコメントを書ける。
  • 2020年代の時点で推奨される単体テストのフレームワークを使って、継続的なテストを実施している。
  • github に付随して継続的なテストを自動化できている。

かつてのC/C++のコーディングの状況(2005年頃)

  • 限られたリソースで静的にメモリを確保する組み込みプログラム
  • 起動時点ですでにメモリを確保させることで、起動時間の短縮。
  • 動的なメモリ確保にともなう不安定さの回避。
  • そのような中で、使用するメモリの総量が限られている中で、動作させていた。
    当- 時、C言語で各種数学のアルゴリズムを活用する際には、「Numerical Recipes in C 日本語版」
    を参照していた。そのため、コンパイル時点では配列の大きさは確定していることを前提としていた。
    配列の大きさをハードコーディングするのが嫌ならば、#define N 100
    などとマクロ定数を使うしかなかった。

使ったことがあるライブラリの範囲は、人それぞれの異なる。

  • C/C++でのコーディング経験は人によって著しく偏る。
  • windows.hを必須とする人から、数値計算・画像処理だけしかしたことのない人などさまざまだ。

私がC++を書く場合のアプローチ

  • 実行させたい内容を明示的にする。
  • Pythonやその他の言語で、プロトタイプを作る。
  • testする。
  • profileをとる。
  • 高速化のために何をする必要があるのかを見極める。
  • その言語の範囲で高速化をする。
  • testする。
  • profileをとる。
  • C++でどう書くべきかを明確にする。
  • C++で同じ機能を実現するために使えるライブラリを調査する。
  • そのライブラリを使って、目的の機能を実装する。
  • testする。
  • profileをとる。

OpenCV の場合のガイドライン

The code should be written C++ 11.
All the functionality must be put into cv:: namespace, or nested namespace, e.g. cv::vslam::
などといったことが書いてある。
たぶん、他のプロジェクトでも有効なガイドラインが多いだろう。
詳しくは、以下のリンクを見てほしい。
https://github.com/opencv/opencv/wiki/Coding_Style_Guide

ROS C++ スタイルガイド

  • #ifdef DEBUG ではなく、#if DEBUG を使う。
  • 多重継承は度を越えた混乱を引き起こす可能性があるため、禁止に近い非推奨です。
  • 静的クラス変数は非推奨です。これはコードの複数インスタンス化を阻害し、マルチスレッドプログラミングを悪夢に変えます。
  • ライブラリ内でexit()を呼び出してはいけません。
  • 大域変数(グローバル変数)は基本的に使うべきではありません。

Google C++ スタイルガイド 日本語全訳

C++でアルゴリズムを構築するうえで大事なこと

  • まず大事なアルゴリズムを検証すること
  • 信頼性のあるライブラリを使ってアルゴリズムを検証すること
  • アルゴリズムのテストを充実させること。
    − プロファイリングをして実行速度上の不具合を減らしておくこと。
    − この時点でのMATLABやPython言語での実装は、適切な開発をするうえでの土台になる。
    − 重要なリファレンスデザインだ。

C++でのアルゴリズムの構築の前に、別言語でアルゴリズムを十分にテストする。

  • MATLABやPython言語でリファレンスデザインがある状況だと、ひとつひとつのやるべきことが明確になっている。
  • MATLABやPython言語で実現している部分をC++のライブラリで実装するときに、使えるライブラリを調査する。
  • Pythonでopencvを使っている場合には、C++でもOpenCVを使うのが自然な選択だろう。
  • 結果の可視化の部分には、Python言語で書いたライブラリをそのまま転用することもできるだろう。

実装したいアルゴリズムに対する理解こそが重要。

  • C++で実装したいなら、C++で利用するライブラリの品質を確かめよう。
  • C++の数値計算ライブラリを自作してはいけない。
  • リファレンスデザインの実行結果とテストすることでロジックの妥当性を確保できる。
  • C++ での実装をプロファイルしよう。

業務としてのC++のコードを書く場合

  • 想定していい言語仕様の範囲を明らかにすること。
  • 使っていいライブラリの範囲を明らかにすること。
  • チームとして使用する単体テストフレームワークの明示
  • 安全なコーディングをするためのチームのガイドラインを共有すること。
  • ポインタ渡しよりは参照渡しを用いること。
  • その業界で採用されるコーディング規約に従うこと。
  • ドキュメンテーションコメントの流儀をチームの流儀にそろえること。
  • 一連のモジュールでのエラーの扱いについて方針をそろえること。
  • 例外の扱い方をそろえる。

極力使わないであろうC言語由来の機能

  • #ifdef は私もなるべく使わないだろう。
    qiita 機能を切り替えるための #ifdef は最小限にしよう
  • 配列の大きさを与えるための#define
  • C++ では constを使えば、配列の大きさを与えるのに使える。
  • それC++なら#defineじゃなくてもできるよ
  • マクロ関数を自分では定義しない。
  • 参照渡しが可能なときには、ポインタ渡しを使わない。
  • vectorで十分なときに、配列は使わない。
  • C言語自体のchar[] としての文字列を使わない。#include <string.h> することもしない

C++についてもっと知りたいのに、理解しきれていない部分

  • マルチコアのCPUを使ううえでのcoding
    • OpenCV でOpenMPをenable にする方法はわかるのだが、それ以外のライブラリでのマルチコアの活用方法を理解していない。
  • マルチスレッドを使うための2023時点での推奨の方法

組み込み分野のCの例

MISRA C:2023 と MISRA C:2012 AMD 4 の概要

MISRA-C++

そういった分野ごとの品質を高める努力に準拠することなしに、品質を高めたコードを作ることはできません。
組み込み分野のC++ならば

  • assertで終了してはいけない。
  • 本当に許させる例外でなくては、例外を発生させてはならない。
  • メモリーのトラブルを生じないことを、コーディングとコンパイルの時点で明らかにできるやり方をしなくてはならない。

業務の種類によってC++のコーディングの位置づけは違ってくる。

ソフトウェアエンジニアであるということは、どのような内容のソフトウェアでも書けるということを意味しない。画像処理・画像認識・数値計算・機械学習分野のソフトウェアを書く人は、もっぱら、その分野のライブラリについてだけ詳しく、異なる分野のライブラリについては、つど学習するしかない。
画像認識・機械学習の分野のソフトウェア作成は、最終の実装コードのコーディングだけを意味しない。画像認識・機械学習の分野では、そのアルゴリズムの妥当性を検証する作業などの方が比率として大きい。

[Google C++ スタイルガイド 日本語全訳]
(https://ttsuki.github.io/styleguide/cppguide.ja.html)

Q: C/C++のコーディングの書式をそろえるformatterは何がおすすめですか

Q: 単体テストは何を使ってますか

C++ のバージョン

C++11
C++14
C++17
C++20

これらのバージョンの違いを述べられるほど、私はC++を知らない。

C++の劇的な進化に圧倒されるModern C++

C++20スマートポインタ入門

new delete も もはや推奨されない。

かつての当たり前が通用しない? Modern C++ではやらないこと第1回 newもdeleteも呼ばないで!
なぜC++プログラマはnew演算子の使用を最小限にとどめなければならないのですか?

C++11の標準ライブラリを使った並列化

C++ へようこそ - Modern C++

C++20 の書籍の案内のwebページ

C++14 以降の本の案内がある。
https://cppmap.github.io/learn/books/

採用者側で意識してほしいこと。

  • C++での現時点でのコーディング能力をはかることにどれだけ意味がありますか。Pythonなどのその他の言語でのコーディング能力をはかることで代用できませんか?

  • 現時点でC++をコーディングしていることを、かつてC++でコーディングの経験があることとの違いが重要かどうかを区別しましょう。

  • 言語仕様の理解と実装能力と、オブジェクト指向設計の実装能力のどちらか見たいのかをはっきりさせよう。

  • 安易なC++のコーディングテストは、専門性の高さはないけれどもC++を日頃書いている人が有利にはたらく。実装する対象についてのアルゴリズムに詳しく、テストやprofileも含めて推進できるエンジニアを取りこぼす可能性がある。

  • レガシーを引きずりすぎた言語。

  • 推奨されないライブラリが多数含まれている言語

    • スレッドセーフでないライブラリの存在
    • 文字コードの標準化以前からのコードの存在
  • 処理系依存の問題

  • 想定していい言語規格の範囲

開発チームで意識すべきこと

どういう流儀で開発しているのかを示そう。

  • OSの有無: 例 Ubuntu 20

  • C++の規格:例 C++14 以降

  • コーディングスタイル: 例 Autowareコーディング規約

  • 単体テストにフレームワーク: GoogleTest

  • 業界標準のライブラリ: Autoware

  • そうすることで、適切なライブラリ、適切なコーディングスタイルの重要性がわかっている人が応募してくれる可能性がたかまります。

  • チームとしてのソフトウェアでの開発では、我流でのコーディング流儀は推奨されません。

どのような開発スタイルを開発チームでしているかを示すことは、その場所が、デスマーチを引き起こしやすいチームか、そうでないのかを示すものになります。
ちゃんとした開発をしているチームのメンバーになりたいものです。

C++ のエンジニアを雇い入れて、コードを開発する際にはたとえば、次のような具合にしてコードのスタイル・品質を確保するのだろう。

  • C++11以降のバージョンで、
  • コーディング規約を明示して
  • フォーマッタを指定して、書式をそろえる。
  • CircleCIなどのツールで、google test などの自動化テストが動作するようにしておく。
  • doxygenで処理できる javadoc スタイルのdocumentation commentを用いる。
  • 高速化のためのマルチスレッドは、std::thread を用いる。

C++で使いたいライブラリ

しっかりしたcoding guideline を採用している場所で働こう

  • C++では、ちゃんとしたコーディングのガイドラインを採用して書かない限り、泥沼にはまります。
  • C++では、なおさらちょっとの改変(#include )といったものでさえ、動作を変えてしまいます。
  • C++でも、標準的なテストツールを使ってテストをしないというのは考えられません。
  • C++でも、CIを必要とします。
  • そういったところで仕事をするのか、人月商売の会社で働くかによって、あなたの成果は変わってきます。

MATLABからC++を生成するという可能性

  • MATLABのような簡潔な言語でロジックを記載しておいて、アルゴリズムのテストをする。
  • その後、そのアルゴリズムにしたがって、C++を記述するというアプローチを述べた。
  • その延長上にMATLABのコードからC++を生成する方法があるということだ。
  • どこまでそれが実際に可能なのかは、MATLABを使って評価してみよう。

MATLAB から C++ コードを配布

例:自動車分野のMATLABのtoolbox blockset

Screenshot from 2023-09-23 23-22-45.png
上記は 以下のサイトの スナップショット https://jp.mathworks.com/products.html

まとめ

あなたの部署でのC++のソフトウェアエンジニアの採用基準でのC++コーディングテストは、採用したい人物を見極めるための有効な基準になっていますか。

日本語を書けるということと、小説を書けるということの違い

− 小説を書くのは日本語を書ければ誰にでもできるというものではない。

  • 小説を書くには、小説に書くべき何かがあって、小説という作法で日本語で書く必要がある。

  • 小説を書くには、誰かしらの読み手を想定して、読み手に届くように書く必要がある。

  • 私は日本語を書けるからといって、日本語で小説を書けるわけではない。

  • C++でソフトウェアを書くといっても、ソフトウェアで実現すべき何かを持っていて、それを実現できるたけのアルゴリズム・プロトコール・ライブラリについて知識をもっていないと、書けるものではない。C++のソフトウェアを書けるからといって、WEBブラウザを書けるわけではない。またC++でWEBブラウザを通常の人が書くべきでもない。

  • C++を書けることとある目的のコードを書けるとは、大きな違いがある。

参考情報

SEC BOOKS:ESCR C++ Ver. 2.0:【改訂版】組込みソフトウェア開発向けコーディング作法ガイド[C++言語版] Ver. 2.0
SEC BOOKS:ESCR Ver. 3.0:【改訂版】組込みソフトウェア開発向け コーディング作法ガイド[C言語版]ESCR Ver.3.0

以下の動画はC言語を教えるのをやめようというCppCon 2015 での動画だ。
CppCon 2015: Kate Gregory “Stop Teaching C"

コーディング規約の例

[Autowareコーディング規約]
(https://github.com/streetdrone-home/Autoware/blob/master/docs/jp/coding-standard.md)
[ROS C++コーディング規約]
(http://wiki.ros.org/CppStyleGuide)

例をあげれば、次のような注意が書かれている。

​グローバル
グローバル (変数と関数の両方) は推奨されません。 これらは名前空間を汚染し、コードの再利用性を低下させます。

特にグローバル変数は使用しないことを強くお勧めします。 これらは、コードの複数のインスタンス化を妨げ、マルチスレッド プログラミングを悪夢にします。

ほとんどの変数と関数はクラス内で宣言する必要があります。 残りは名前空間内で宣言する必要があります。

例外: ファイルには、main() 関数と、グローバルないくつかの小さなヘルパー関数が含まれる場合があります。 ただし、これらのヘルパー関数がいつか他の人にとって役立つようになる可能性があることに注意してください。

他にも 他重継承はするなと書いてある

[clang-formatを用いた自動フォーマット]
(https://github.com/davetcoleman/roscpp_code_format)
"{" の位置などをめぐる無用な論争を避けられる。

Rustという選択肢


蛇足

  • 以下のコードの例は、空白文字で区切られた単語の数をカウントするC++のコードをChatGPT に問い合わせた結果である。

chatGPT の出力

#include <stdio.h>
#include <stdbool.h>

// 文字がスペース、タブ、または改行コードかどうかを判定するヘルパー関数
bool isSeparator(char c) {
    return c == ' ' || c == '\t' || c == '\n';
}

// stdinから入力された文字列から単語の数を数える関数
int countWords() {
    char c;
    int wordCount = 0;
    bool inWord = false;

    // EOFに達するまで1文字ずつ読み込む
    while ((c = getchar()) != EOF) {
        // 現在の文字がスペース、タブ、または改行コードである場合
        if (isSeparator(c)) {
            inWord = false; // 単語内にいないことを示すフラグをセット
        }
        // 現在の文字がスペース、タブ、または改行コードでない場合
        else {
            // 単語内にいない場合、新しい単語が始まったことを示すフラグをセットし、単語の数を増やす
            if (!inWord) {
                inWord = true;
                wordCount++;
            }
        }
    }
    return wordCount;
}

int main() {
    printf("文字列を入力してください(Ctrl+DまたはCtrl+Zで入力終了):\n");
    int wordCount = countWords();
    printf("入力された単語の数: %d\n", wordCount);
    return 0;
}
#include <iostream>
#include <string>

// 文字がスペース、タブ、または改行コードかどうかを判定するヘルパー関数
bool isSeparator(char c) {
    return c == ' ' || c == '\t' || c == '\n';
}

// stdinから入力された文字列から単語の数を数える関数
int countWords() {
    std::string input;
    int wordCount = 0;
    bool inWord = false;

    // 1行ずつ読み込む
    while (std::getline(std::cin, input)) {
        for (char c : input) {
            // 現在の文字がスペース、タブ、または改行コードである場合
            if (isSeparator(c)) {
                inWord = false; // 単語内にいないことを示すフラグをセット
            }
            // 現在の文字がスペース、タブ、または改行コードでない場合
            else {
                // 単語内にいない場合、新しい単語が始まったことを示すフラグをセットし、単語の数を増やす
                if (!inWord) {
                    inWord = true;
                    wordCount++;
                }
            }
        }
    }
    return wordCount;
}

int main() {
    std::cout << "文字列を入力してください(Ctrl+DまたはCtrl+Zで入力終了):\n";
    int wordCount = countWords();
    std::cout << "入力された単語の数: " << wordCount << std::endl;
    return 0;
}
#include <iostream>
#include <string>
#include <sstream>

// 1行をスペースやタブで区切って単語の数を数える関数
int countWordsInLine(const std::string& line) {
    std::istringstream iss(line);
    int wordCount = 0;
    std::string word;

    // スペースやタブで区切られた単語を数える
    while (iss >> word) {
        wordCount++;
    }

    return wordCount;
}

// stdinから入力された文字列から単語の数を数える関数
int countWords() {
    std::string input;
    int totalWordCount = 0;

    // 1行ずつ読み込む
    while (std::getline(std::cin, input)) {
        int wordCountInLine = countWordsInLine(input);
        totalWordCount += wordCountInLine;
    }

    return totalWordCount;
}

int main() {
    std::cout << "文字列を入力してください(Ctrl+DまたはCtrl+Zで入力終了):\n";
    int wordCount = countWords();
    std::cout << "入力された単語の数: " << wordCount << std::endl;
    return 0;
}

途中から参考にした記事

以下の記事は、業務でC++を使いこなしている方による記事です。
私のような未熟者とは異なるレベルの記事になっています。

63
53
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
53