C++ ヘッダ関数の重複定義を解決する方法|already defined / multiply defined symbolsとinline・ODRの正しい理解

Visual StudioでC++をビルドしたら「already defined」「one or more multiply defined symbols」などのリンカエラーが出る——原因は“ヘッダに書いた関数定義”が複数の.cppに複製されることです。本記事ではinlineで直る理由(ODRと翻訳単位)から、正しい修正パターンと実務的な判断基準まで整理します。

目次

起きていること:コンパイルは通るのに「リンク」で落ちている

提示されているメッセージのうち、次の2つは典型的なリンカ(linker)由来のエラーです。

  • already defined in ○○.obj
  • one or more multiply defined symbols found

Visual Studio(MSVC)だと、実際には以下のようなエラー番号(例:LNK2005 / LNK1169)が付くことが多いです。

error LNK2005: "..." は既に WinApp.obj で定義されています
fatal error LNK1169: 1 個以上の多重定義されたシンボルが見つかりました

重要なのは、これは「文法ミス」や「型の不一致」ではなく、最終的に1つの実行ファイル(またはDLL)へ結合するときに、同じ関数が複数個見つかったというエラーだという点です。


結論:ヘッダに関数の「定義(本体)」を書くならinline、そうでないなら.cppへ移す

今回の現象は、原因と対処が比較的はっきりしています。

  • 再利用する普通の関数:ヘッダには宣言だけ、定義は1つの.cppに置く
  • ヘッダに置く必要がある関数(ヘッダオンリー、テンプレート、軽量ヘルパー等):定義にinlineを付ける

「inlineを付けたら直った」のは偶然ではなく、C++のルール上、そうなるべくしてそうなっています。


まず整理:宣言と定義は別物

同じ「関数を書く」でも、C++では宣言(declaration)と定義(definition)が別扱いです。

種類例役割ヘッダに置くのは?
宣言std::string f(const wchar_t*);「こういう関数が存在する」と知らせる基本OK
定義std::string f(...) { ... }関数本体そのもの(実体)条件付き(inline等が必要)

今回の問題は、ヘッダに「宣言」ではなく定義(本体)を書いていることが出発点です。


根本原因:ヘッダに定義を書いたまま複数の.cppからincludeしている

質問の関数は次のようなイメージでした(簡略化)。

std::string WCHAR_TO_STRING(WCHAR* wc, int& chars)
{
    std::string ws = "";
    return ws;
}

これがヘッダ(例:MyFunctions.h)に書かれていて、複数の.cppがそれを取り込むと、内部的には次の状態になります。

  • WinApp.cpp は MyFunctions.h を展開してコンパイルされる
  • Other.cpp も MyFunctions.h を展開してコンパイルされる
  • つまり関数本体が.cppごとに“コピー”される

ここでポイントになる概念が翻訳単位(translation unit)です。C++では、ざっくり言うと次の流れでビルドされます。

工程何が起きるか今回の問題との関係
プリプロセス#includeを“貼り付け”て1つの大きなソースにするヘッダの関数定義が各.cppに入り込む
コンパイル翻訳単位ごとに.obj(オブジェクトファイル)を作る各.objに同名の関数シンボルが生成される
リンク複数の.objを結合して.exe/.dllにする同じ関数が複数あるため衝突してエラーになる

ここで大事な誤解を1つ潰しておきます。

includeガード(#pragma once / #ifndef)は万能ではない

「ヘッダには#pragma onceを書いているのに、なぜ重複するの?」という混乱は非常によくあります。

#pragma onceやincludeガードが防げるのは、“同じ翻訳単位の中で”同じヘッダを二重に取り込むことだけです。別の.cppは別の翻訳単位なので、そこまで止められません。

// MyFunctions.h
#pragma once
std::string WCHAR_TO_STRING(WCHAR* wc, int& chars) { ... }

このヘッダを10個の.cppがincludeしたら、10回分の定義が生成されること自体は変わりません。


ODR(One Definition Rule):非inlineの関数定義は「プログラム全体で1つだけ」

C++にはODR(One Definition Rule)というルールがあり、ざっくり言うと次が要点です。

  • 外部リンケージを持つ非inlineの関数定義は、プログラム全体で1回だけでなければならない
  • これに違反すると、リンク時に「多重定義」として弾かれる(または条件によっては未定義動作の温床になる)

ヘッダに「非inlineの関数定義」を置いて、複数の.cppがincludeする行為は、意図せずしてODR違反を起こしやすい典型パターンです。


inlineを付けるとなぜ直るのか:inlineは“インライン展開”だけの意味ではない

質問では、次のように書き換えるとエラーが消えたとのことでした。

inline std::string WCHAR_TO_STRING(WCHAR* wc, int& chars)
{
    std::string ws = "";
    return ws;
}

これが効く理由は、inlineが持つ意味が2つあるからです。

inlineの意味よくある誤解実際に重要な点
最適化ヒント「必ずインライン展開される」実際はコンパイラが決める(inlineでも展開しないことは普通にある)
リンケージ上の特別扱い知られていないことが多い同一の定義が複数の翻訳単位にあっても許される(ただし中身は一致が必要)

今回の「already defined / multiply defined symbols」はまさに後者です。inlineを付けることで、ヘッダ経由で複数の.objに同じ関数が現れても、規格上「それは許される形」とみなされ、リンカも1つにまとめられる(多くの処理系ではCOMDAT/weak的な仕組みで統合される)ため、衝突が起きなくなります。

注意:inlineなら何でもOK、ではない

inlineで許されるのは、あくまで“同一の定義”が複数の翻訳単位に現れるケースです。次のようなパターンは地味に危険です。

  • マクロや#ifdefで、翻訳単位ごとに関数本体が微妙に変わる
  • ビルド設定差で、同じヘッダでも一部の.cppだけ違うコードが生成される

この状態でinlineにすると、リンクは通ってしまうのに、実行時に想定外の動きになる(規格的には未定義動作に近い領域に踏み込む)ことがあります。ヘッダに置くinline関数は、どの.cppから見ても同じ定義になるように保つのが鉄則です。


関数本体が16行でもinlineにして問題ない?実務で見る判断軸

「inlineは短い関数だけ」というイメージが残っていることがありますが、現代のコンパイラ最適化では、行数はほぼ本質ではありません。重要なのは次の観点です。

パフォーマンス面:inline指定≠インライン展開の強制

inlineを付けても、コンパイラは状況に応じて展開しない判断をします(特にデバッグビルドでは展開されないことも多いです)。つまり、

  • 「16行だから遅くなる」→ 直結しない
  • 「inlineにすると必ず速くなる」→ そうとも限らない

というのが現実です。今回のinlineは、速度目的というよりODR上の“多重定義を許すための指定”として理解するのが実務的です。

ビルド・保守面:ヘッダに本体を置くコストはある

一方で、ヘッダに本体を置く(=多くの.cppが取り込む)設計には、速度よりも開発体験に影響するコストがあります。

観点ヘッダにinline実装.cppに実装
再ビルド範囲ヘッダ変更で依存する多数の.cppが再コンパイルされやすい実装変更でもその.cpp中心で済みやすい
可読性小さなヘルパーは追いやすいが、大きいと散らかりがちインターフェース(宣言)と実装が分離され読みやすい
バイナリサイズ最適化状況によっては増減あり(過度な展開で増えることも)比較的安定

結論として、16行程度のユーティリティ関数をヘッダに置いてinlineにするのは珍しくありません。ただし「将来も頻繁に変える」「重い処理に育ちそう」「外部公開API」なら、宣言と定義を分ける方が長期的に安全です。


「なぜこの関数だけ重複定義扱いになるのか?」でよくある理由

「他にも似た関数があるのに、これだけがODR違反になる」という現象は、次のどれかが当てはまるケースが多いです。

クラス定義内に書いたメンバ関数は“暗黙にinline”

例えば以下は、見た目はヘッダに実装があるのに、ODR的にはinline扱いです。

struct Util {
    std::string f(WCHAR* wc, int& chars) { return ""; } // これは暗黙inline
};

このタイプの関数は多重定義エラーになりません。つまり、プロジェクト内の「他の20個の関数」は実はこのパターンだった、というのが非常にありがちなオチです。

テンプレート関数はヘッダに定義が必要(そして多くの場合inline的に扱われる)

テンプレートは使用箇所で実体化される都合上、ヘッダに定義を書くのが自然です。テンプレートが混じっていると「ヘッダに定義でも平気な関数」が存在するため、違いが分かりにくくなります。

constexpr(やconsteval)の関数は暗黙にinline

constexpr関数は規格上、暗黙にinlineになります。これも「ヘッダに本体を書いても大丈夫な関数」を増やします。

その関数を含む.cppが“たまたまリンク対象になっていなかった”

静的ライブラリや条件付きビルドが絡むと、「同じヘッダをincludeしていても、リンクに参加する.objが片方だけ」という状況が起きえます。すると重複が表に出ず、あるタイミング(参照が増えた、構成が変わった、別プロジェクトからリンクした等)で急に噴出します。

“フリー関数+ヘッダ定義+非inline”はエラーが顕在化しやすい

結局のところ、今回のWCHAR_TO_STRINGは

  • フリー関数(名前空間スコープ)
  • ヘッダに定義を書いた
  • inlineもstaticも付いていない

という「多重定義の王道」になっていたため、そこだけリンクエラーとして表に出た、と考えるのが自然です。


実務で使える修正パターン

宣言と定義を分ける(最も堅実)

公開API・大きめの関数・頻繁に変える可能性がある実装は、この形が最も事故が少ないです。

// MyFunctions.h
#pragma once
#include <string>
std::string WCHAR_TO_STRING(const WCHAR* wc, int& chars);
// MyFunctions.cpp
#include "MyFunctions.h"

std::string WCHAR_TO_STRING(const WCHAR* wc, int& chars)
{
    std::string ws = "";
    return ws;
}

他の.cppはヘッダをincludeして呼ぶだけです。

// Other.cpp
#include "MyFunctions.h"

void test()
{
    int chars = 0;
    std::string s = WCHAR_TO_STRING(L"abc", chars);
}

この方式なら、定義はプロジェクト全体で1か所なので、ODR違反の余地がほぼ消えます。


ヘッダに置きたいならinlineを付ける(ヘッダオンリーの定番)

軽量ヘルパー・ユーティリティ・テンプレートとセットの補助関数など、「ヘッダに置く理由」がある場合はこのパターンです。

// MyFunctions.h
#pragma once
#include <string>

inline std::string WCHAR_TO_STRING(const WCHAR* wc, int& chars)
{
    std::string ws = "";
    return ws;
}

このときのinlineは、速度よりも「同一定義の多重出現を許可する」意味合いが主です。


staticを付ける(内部リンケージにして衝突を回避)

staticを付けると、その関数は翻訳単位(=各.cpp)ごとに閉じた存在になり、リンカから見ると別物になります。

// MyFunctions.h
#pragma once
#include <string>

static std::string WCHAR_TO_STRING(const WCHAR* wc, int& chars)
{
    std::string ws = "";
    return ws;
}

この方法は確かに重複定義エラーを消しますが、実務では次の点を理解した上で選びます。

  • 各.cppごとに関数のコピーが作られやすい(コード重複・サイズ増の可能性)
  • 「プロジェクト全体で共通の1つの関数」にしたい用途には向かない

“その.cpp内だけで使うヘルパー”をヘッダに置くという設計自体が少し不自然なので、基本は「そのヘルパーを.cpp側に移す」か「inlineで共有する」方が分かりやすいことが多いです。


無名名前空間(static相当)

無名名前空間も内部リンケージになります。ただし、ヘッダに無名名前空間を置くのは慎重に扱うのが実務では一般的です(翻訳単位ごとに別の名前空間=別の実体になるため、型やオブジェクトが絡むと地雷になりやすい)。

// (推奨は.cpp内)MyFunctions.cpp 側で
namespace
{
    std::string WCHAR_TO_STRING(const WCHAR* wc, int& chars)
    {
        std::string ws = "";
        return ws;
    }
}

「外に見せたくない」「ファイルローカルにしたい」という意図なら、無名名前空間は非常に有効です。ただし置き場所はヘッダではなく.cppに寄せるのが安全です。


どれを選ぶべきか:迷ったときの判断表

状況おすすめ理由
複数の.cppから使う共通関数 / 公開API宣言は.h、定義は1つの.cppODR違反を避けやすく、ビルド影響も局所化
ヘッダオンリーで提供したい / 小さなヘルパーヘッダ定義 + inline多重定義を許可しつつ共有できる
その.cppでしか使わない内部ヘルパー.cpp内に置く(無名名前空間)意図が明確で衝突もしない
どうしてもヘッダに置きたいが外部に見せたくない設計見直し(まず.cppへ)ヘッダ内部リンケージは複雑化しやすい

補足:[[nodiscard]]の警告は別件(リンクエラーの原因ではない)

ログに混じっていた

Warning: discarding return value of function with [[nodiscard]] attribute

は、戻り値を捨ててはいけない(捨てると警告する)という属性[[nodiscard]]が付いた関数の戻り値を、呼び出し側が無視したときに出ます。これは多重定義(already defined / multiply defined symbols)とは別問題で、たとえば以下のような呼び出しがあると発生します。

WCHAR_TO_STRING(wc, chars);  // 戻り値を使っていない(例)

戻り値が必要なら変数に受け、不要なら「意図的に捨てている」ことを示す書き方にします。

// 使う
std::string s = WCHAR_TO_STRING(wc, chars);

// 捨てる(意図を明確化)
(void)WCHAR_TO_STRING(wc, chars);

ただし、あなたのWCHAR_TO_STRING自体に[[nodiscard]]を付けていないなら、警告の対象は別の関数である可能性もあります。リンクエラーの調査と切り分けると、原因が追いやすくなります。


ついでに改善:Windowsの文字変換は「何のエンコーディングにするか」を明確に

今回の本題は多重定義ですが、WCHAR*をstd::stringへ変換する関数は、実務では次の点でつまずきやすいです。

  • WCHAR(UTF-16)をstd::string(バイト列)にするとき、結果のエンコーディング(UTF-8 / Shift_JIS等)を決める必要がある
  • 入力がnull終端なのか、文字数charsで指定されるのかを統一したい
  • 引数は基本const WCHAR*にしておく方が安全(入力を破壊しないことを型で示せる)

例えば「UTF-16(WCHAR)→ UTF-8(std::string)」を明確にするなら、Windows APIのWideCharToMultiByteを使う設計が定番です(ここでは概念例として載せます)。

#include <string>
#include <windows.h>

inline std::string WideToUtf8(const WCHAR* wc, int chars)
{
    if (!wc) return std::string();

    // chars が -1 の場合は null終端、正の値ならその文字数を変換、など運用を決める
    int required = WideCharToMultiByte(CP_UTF8, 0, wc, chars, nullptr, 0, nullptr, nullptr);
    if (required <= 0) return std::string();

    std::string out(required, '\0');
    WideCharToMultiByte(CP_UTF8, 0, wc, chars, out.data(), required, nullptr, nullptr);
    return out;
}

このように「どのエンコーディングのstringなのか」を関数名・コメント・定数で明示しておくと、後からの不具合(文字化け、長さ計算ミス、null終端の扱いなど)を減らせます。もちろん、この関数をヘッダに置くなら今回の話と同じくinlineが必要です。


まとめ:今回のエラーは“ヘッダ定義 + 非inline”が引き起こすODR違反

  • ヘッダに非inlineの関数定義を書くと、複数.cppからincludeした時点で翻訳単位ごとに定義が複製される
  • その結果、リンク時にalready defined / multiply defined symbolsとして衝突する(ODR違反)
  • inlineを付けると、同一定義が複数翻訳単位に現れても許され、リンカが統合できる
  • 16行かどうかは本質ではなく、inlineは「最適化」よりも「リンケージ上の許可」の意味が重要
  • 迷ったら、公開・共通関数は.cppに実装、ヘッダに置く必要があるものはinlineが堅実

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次