🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

3回答

3124閲覧

C++ オブジェクトの逆シリアル化を行うとメモリがずれる

yoshiki_iwasa

総合スコア23

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2020/12/16 05:12

編集2020/12/16 07:14

##質問概要
・オブジェクト Data のシリアル化と逆シリアル化を行う関数を作成しています。しかし、オブジェクトのシリアル化をした後に、逆シリアル化を行うと、シリアル化した際のメモリレイアウトがずれてしまい、正しく逆シリアル化できないのですが、どうしてそうなってしまうのかわかりません。お力を貸してください。

##質問詳細

環境は、
機種名: MacBook Air
機種ID: MacBookAir8,2
プロセッサ名: Dual-Core Intel Core i5
プロセッサ速度: 1.6 GHz
プロセッサの個数: 1
コアの総数: 2
二次キャッシュ(コア単位): 256 KB
三次キャッシュ: 4 MB
ハイパー・スレッディング・テクノロジ: 有効
メモリ: 8 GB

です

今回、シリアル化したいオブジェクトは、以下のものです

C++

1typedef struct Data 2{ 3 std::string s1; 4 int n; 5 std::string s2; 6} Data; 7

このオブジェクトに関して void *serialization(std::string s1, int num, std::string s2) でシリアル化した後、 Data *deserialization(void *raw) で逆シリアルかすることを考えます

実際のコードと、それをテストした結果をいかに示します。
###実行コード

C++

1// main.cpp 2 3#include <iostream> 4#include <string> 5 6typedef struct Data 7{ 8 std::string s1; 9 int n; 10 std::string s2; 11} Data; 12 13// serialization 関数名で、先に52バイトメモリを確保し、キャストしながらメモリに値を配置していく 14void *serialization(std::string s1, int num, std::string s2) 15{ 16 void *ret = reinterpret_cast<void*>(new char(52)); 17 void *tmp; 18 std::string *p_s1; 19 int *p_num; 20 std::string *p_s2; 21 22 p_s1 = reinterpret_cast<std::string*>(ret); 23 std::cout << "p_s1 = " << p_s1 << std::endl; 24 *p_s1 = s1; 25 p_s1++; 26 p_num = reinterpret_cast<int*>(p_s1); 27 std::cout << "p_num = " << p_num << std::endl; 28 29 *p_num = num; 30 p_num++; 31 p_s2 = reinterpret_cast<std::string*>(p_num); 32 std::cout << "p_s2 = " << p_s2 << std::endl; 33 34 35 *p_s2 = s2; 36 37 return (ret); 38 39} 40 41Data *deserialization(void *raw) 42{ 43 Data *ret; 44 ret = reinterpret_cast<Data*>(raw); 45 return (ret); 46} 47 48int main() 49{ 50 51 std::cout << "\n\n======================Serialized Raw Data========================\n\n" << std::endl; 52 53 void *p = serialization("testtest", 42, "testtest"); 54 55 56 std::cout << "\n\n======================Deserialized Raw Data========================\n\n" << std::endl; 57 58 Data *data = deserialization(p); 59 60 std::cout << "p_s1 = " << &data->s1 << std::endl; 61 std::cout << "p_s1 = " << &data->n << std::endl; 62 std::cout << "p_s1 = " << &data->s2 << std::endl; 63 std::cout << data->s1 << std::endl; 64 std::cout << data->n << std::endl; 65 std::cout << data->s2 << std::endl; 66} 67 68

###実行結果

% clang++ main.cpp % ./a.out ======================Serialized Raw Data======================== p_s1 = 0x7f9388c05910 <- 10 (16) p_num = 0x7f9388c05928 <- 28 (40) (std::string の24byte分メモリが進んだ) p_s2 = 0x7f9388c0592c <- ここは、2c(44) なのに (int の 4byte分メモリが進んだ) ======================Deserialized Raw Data======================== p_s1 = 0x7f9388c05910 <- 10 (16) p_s1 = 0x7f9388c05928 <- 28 (40) p_s1 = 0x7f9388c05930 <- ここは、30(48) に変わって4バイト分余計にずれている testtest 42 test <- 値も失われている

####まとめ

このように、シリアル化してから逆シリアル化するさいにメモリがずれデータが失われるのはなぜでしょうか...

自分の予想としては、逆シリアル化するさいに単純なreinterpret_cast をしているのが問題なのかな と予想してますが、何が問題なのかわかりません。

Data オブジェクトの3つのメンバはメモリ上に一列に配置されるので、シリアライズされ送られてきたポインタをキャストすれば逆シリアル化できると思ってました。

違うようです...

ご教授くださいm(_ _)m

補足

Data オブジェクトの std::string には、最大で小英数字8 文字までしかいれない前提があります。。
なので、std::string を 24byte として決め打ちしてます

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

episteme

2020/12/16 05:22

std::string の大きさ(長さ)は不定/可変なのに、なんで52バイト決めうちで領域確保できるんですか?
yoshiki_iwasa

2020/12/16 05:34

あ、ご指摘ありがとうございます! すいません 記載漏れがありました。 Data の string メンバに格納される文字列は、最大で小英数字8文字までという前提があります。 この前提に立つと、std::string は 24バイトで決め打ちして問題なくなります
episteme

2020/12/16 06:06

> この前提に立つと、std::string は 24バイトで決め打ちして問題なくなります 文字列長が8に満たないときも8byteを文字列とするってこと? であれば、そのことがコードで表現されていません。
yoshiki_iwasa

2020/12/16 07:14

紛らわしかったので、8文字に変更しました! ご指摘ありがとうございます
guest

回答3

0

ベストアンサー

参考になるかわからんが、僕ならこう書く(内包する文字列が改行を含まぬASCIIのみで構成されている場合)

C++

1#include <iostream> 2#include <sstream> 3#include <string> 4 5struct Data { 6 std::string s1; 7 int n; 8 std::string s2; 9}; 10 11// Dataを文字列化 12std::string to_string(const Data& data) { 13 std::ostringstream stream; 14 stream << data.s1 << std::endl 15 << data.n << std::endl 16 << data.s2; 17 return stream.str(); 18} 19 20// 文字列をData化 21Data from_string(const std::string& str) { 22 std::istringstream stream(str); 23 Data data; 24 std::getline(stream, data.s1); 25 std::string tmp; 26 std::getline(stream, tmp); 27 data.n = std::stoi(tmp); 28 std::getline(stream, data.s2); 29 return data; 30} 31 32int main() { 33 Data input; 34 input.s1 = "hoge"; 35 input.n = 123; 36 input.s2 = "fuga"; 37 38 // シリアライズ 39 std::string str = to_string(input); 40 41 // デシリアライズ 42 Data output = from_string(str); 43 44 std::cout << '[' << output.s1 << "] " << output.n << " [" << output.s2 << "]\n"; 45}

投稿2020/12/16 07:43

episteme

総合スコア16612

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yoshiki_iwasa

2020/12/17 06:47

回答案をつくってくださってありがとうございます!! さんこうにしてもう少し考えてみます!
episteme

2020/12/17 17:25

さんこうにするのは構わんけど、あなたの抱える問題には答えていません。それでもベストアンサー?
guest

0

言語仕様としてはオブジェクトがどのようなメモリレイアウトになるのかということを限定的な保証しかしていません。 trivially copyable 要件を満たす場合を除いてはバイト単位でのコピーがオブジェクトのコピーとしても正しいとは保証されません。 未定義です。

「どうしてそうなってしまうのか」ということに (言語仕様に基づいた) 説明をするなら「保証されていないことをしているから」というのが解です。


メモリのレイアウトに関すること以外で言えば *p_s1 は std::string として初期化されていません。 代入は必ずしも素朴なビットパターンのコピーでありませんのできちんと初期化されて std::string 型として整合性が取れている状態になっていないと何が起こるかわかりません。

仕様の関連項目

様々なルールの組み合わせなので今回の事情にちょうどよい内容がまとまっているものというとちょっと示せないのですが、とりあえずおおざっぱにだけ仕様 (C++11) の関連個所をリンク付きで抜き出しておきます。 規格を読み解くのは大変なのでわからないところはこのあたりに出てくる用語から適当にググってください。

そして std::string は trivially copyable class ではないのでこれをバイト列としてコピーして戻したときにオブジェクトとして正しいことは保証されないわけです。。

cpp

1#include <iostream> 2#include <type_traits> 3#include <string> 4 5int main(void) { 6 // 0 が出力される。 std::string は trivially copyable ではない。 7 std::cout << std::is_trivially_copyable<std::string>() << std::endl; 8 return 0; 9}

レイアウトに関する保証としては、

言語の仕様として保証していないことであってもすぐさま駄目なわけではなく、処理系・実行環境としてなんらかの保証を与えている場合もあります。 ただ、検証するのが面倒ですし、移植性も損なうので特別に強い理由があるのでなければ言語仕様が保証する範囲内でやるのが好ましくはあります。

投稿2020/12/16 07:36

編集2020/12/17 08:45
SaitoAtsushi

総合スコア5684

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yoshiki_iwasa

2020/12/17 06:48

未定義なんですね 。。。 ありがとうございます! この回答内容に関する参考資料のURLなどあれば教えていただきたいです!!
yoshiki_iwasa

2020/12/18 09:56

ありがとうございます!!! 参考資料に加えて、大量の解説を頂けるとは思ってませんでした 感謝します
guest

0

大きく勘違いをされているようです。
std::stringでの文字列はstring自体に保持されません。
new等で割り当てたヒープ領域に保持されます。

このserializationではメモリ破壊が起こってしまいます。

  1. std::stringのサイズは保持する文字列のサイズと関係ありません。

  stringクラスの実装によりことなります。
私の環境では、sizeofでstd::stringのサイズを出力すると32バイトです。
0. 無理やり、charへのポインタ(newしたメモリ領域)をstd::stringへのポインタとしてキャストしていますが、無理があります。
以下の命令では、s1の内容が*p_s1にコピーされるわけではありません。
std::stringクラスの代入演算子が実行されます。

c++

1*p_s1 = s1;

上記のコードでは、
0. *p_s1にすでに文字列が入っていれば、この文字列の領域を解放
0. s1が保持している文字列コピー

のような、動作になると考えますが、しかし、p_s1は元々、charへのポインタであり、std::stringオブジェクトではないため、動作が不定です。
実際、私の環境ではSegmentation faultが発生しまともに動作しません。

投稿2020/12/16 06:01

編集2020/12/16 08:14
akiruno-oneone

総合スコア815

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

SaitoAtsushi

2020/12/16 06:08

補足しておくと、処理系によっては std::string は短い文字列を保持するときにはオブジェクトの内部にデータをかかえることがあります。 (いわゆる SSO) なので、部分的には上手くいっているように見えてしまうことがあったのかもしれません。
yoshiki_iwasa

2020/12/16 07:25 編集

@akiruno-oneone 回答ありがとうございます。 >std::stringでの文字列はstring自体に保持されません。 new等で割り当てたヒープ領域に保持されます。 このserializationではメモリ破壊が起こってしまいます。 この部分について質問です。 具体的に、どのようなメモリ破壊が起こるのでしょうか..? 自分の理解としては、std::string のためのメモリ24byte は new によって確保されており、std::string が文字列を保持するためのメモリ領域も正しく確保されていると思ってました。 上の実装の場合、どこでどのようなメモリ破壊が起きるのかわかりません...
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問