C++初心者です.
【問題があって → 一応これで問題回避できそうだけど……こんなのでいいのかなぁ? もっとマシなやり方があるのでは?】
みたいな,なんか漠然とした実装方法に関する疑問みたいな話なので,
意見交換とすべきか QA とすべきか迷いましたが,複数の方法論の話がある場合には個々の話が独立した形になった方が良い気がするので後者側にしました.
やりたいこと
なんかこんなの↓があって……
C++
1//何かスタックに積まれるやつ 2struct IStackElem 3{ 4 virtual ~IStackElem() = default; 5 6 //※この型は専らスタックに積まれる使い方をされるという前提であって, 7 //このメソッドが呼ばれるときとは,自身がスタックのtopである場合である. 8 virtual void DoSomething() = 0; 9}; 10 11//スタック 12using Stack_t = std::stack< std::unique_ptr<IStackElem> >;
何らかのタイミングでスタックの top にあるやつの DoSomething()
を実施したい.
C++
1//どこかにスタックがある 2Stack_t TheStack; 3 4//何らかのタイミングでの処理 5void XXX() 6{ 7 /* ... */ 8 9 if( !TheStack.empty() )TheStack.top()->DoSomething(); 10 11 /* ... */ 12}
問題点
スタックに積まれている要素の DoSomething()
内でスタックに対する操作を行いたい のだが,
( DoSomething()
が呼ばれた際には自身がスタックの top だという前提なので)popした時点でその要素自身が破棄されてしまうという点で,実装方法に困っている.
例えば以下は「いくつかpopして→別のをpushする」みたいなのが実装できない状況を示している.
C++
1//(例) 2//DoSomething() にて, 3// まずスタックからいくつかの要素をpopし, 4// その後で新たな要素をpushする 5//ということをしたい. 6struct Problem : public IStackElem 7{ 8public: 9 //Owner : 所属スタック 10 //nPop : popする回数 11 //ToBePushed : pushすべき要素 12 Problem( Stack_t &Owner, int nPop, std::unique_ptr<IStackElem> ToBePushed ) 13 : m_Owner(Owner), m_nPop(nPop), m_ToBePushed(std::move(ToBePushed)) 14 {} 15 16 //ここの実装が問題(これだと実行時に大変なことになる) 17 virtual void DoSomething() override 18 {//m_nPop個だけ pop し,その後で m_ToBePushed を push したいのだが…… 19 20 //(1) pop 21 //m_nPop>=2 のとき,まずこれができない. 22 //(初回の pop で自身が破壊されるので m_Owner や m_nPop を使えない) 23 for( int i=0; i<m_nPop; ++i )m_Owner.pop(); 24 25 //(2) push 26 //こっちも同様に無理. 27 //(自身が破壊されているので m_ToBePushed を使えない) 28 m_Owner.push( std::move(m_ToBePushed) ); 29 }; 30 31private: 32 Stack_t &m_Owner; 33 int m_nPop; 34 std::unique_ptr<IStackElem> m_ToBePushed; 35};
こういう場合,どうすれば良いのでしょうか?
自身で思いつく問題回避方法は「 DoSomething()
でやっていることを全て static メンバ関数に移してやる」なのですが,
もっと他の(何らかの意味で良い?)方法はありませんか?
C++
1 //思いつく問題回避方法 2 virtual void DoSomething() override 3 { 4 //処理を全て static メンバ関数に移した. 5 //必要なものを全て引数として渡して処理を実施する. 6 DoSomething_StaticImpl( m_Owner, m_nPop, std::move(m_ToBePushed) ); 7 }; 8 9private: //staticメンバ関数で処理 10 static void DoSomething_StaticImpl( Stack_t &Owner, int nPop, std::unique_ptr<IStackElem> ToBePushed ) 11 { 12 for( int i=0; i<nPop; ++i )Owner.pop(); 13 Owner.push( std::move( ToBePushed ) ); 14 }
追記
「そもそも要素側からスタックを操作すること自体がどうなのか?」というご指摘,もっともだと思います.
その方面(要素側からスタックを操作しないようにする)で考えると,こんな↓形になりました.
こちらに関しても「いや,そうじゃねぇよ」とか「やるにしてもこうだろう」等々ご教示願えればありがたいです.
C++
1//要素が直接スタックを操作するのではなくて, 2//要素からは「スタックをこのように操作したい」という要求を戻り値で返す. 3//(→で,戻り値を受け取った側でスタックを操作する) 4 5struct IStackElem; 6 7//スタック 8using Stack_t = std::stack< std::unique_ptr<IStackElem> >; 9 10//スタックに対するPush操作 11class Push 12{ 13public: 14 //ctorでPushすべきものを指定 15 Push( std::unique_ptr<IStackElem> ToBePushed ) : m_ToBePushed( std::move(ToBePushed) ) {} 16 //操作実施 17 void Exec( Stack_t &Stk ){ if(m_ToBePushed)Stk.push( std::move(m_ToBePushed) ); } 18private: 19 std::unique_ptr<IStackElem> m_ToBePushed; 20}; 21 22//スタックに対するPop操作 23class Pop 24{ 25public: 26 //ctorでPopすべき回数を指定 27 Pop( size_t nPop=1 ) : m_nPop(nPop) {} 28 //操作実施 29 void Exec( Stack_t &Stk ) 30 { 31 const size_t n = std::min( m_nPop, Stk.size() ); 32 for( size_t i=0; i<n; ++i )Stk.pop(); 33 } 34private: 35 size_t m_nPop; 36}; 37 38//スタックに対する操作要求 39using StackOp = std::variant< Push, Pop >; 40 41//スタックに積まれるやつ 42struct IStackElem 43{ 44 virtual ~IStackElem() = default; 45 46 //※この型は専らスタックに積まれる使い方をされるという前提であって, 47 //このメソッドが呼ばれるときとは,自身がスタックのtopである場合である. 48 // 49 //戻り値として,所属スタックに対して実施されるべき操作シーケンスを返す. 50 virtual std::vector<StackOp> DoSomething() = 0; 51}; 52 53//---------- 54//件の要素例の実装 55struct Problem : public IStackElem 56{ 57public: 58 Problem( size_t nPop, std::unique_ptr<IStackElem> ToBePushed ) 59 : m_nPop(nPop), m_ToBePushed(std::move(ToBePushed)) 60 {} 61 62 virtual std::vector<StackOp> DoSomething() override 63 {//m_nPop個だけ pop し,その後で m_ToBePushed を push したい 64 std::vector<StackOp> OPSeq; 65 OPSeq.emplace_back( Pop(m_nPop) ); 66 OPSeq.emplace_back( Push( std::move(m_ToBePushed) ) ); 67 return OPSeq; 68 }; 69 70private: 71 size_t m_nPop; 72 std::unique_ptr<IStackElem> m_ToBePushed; 73}; 74 75//---------- 76//何らかのタイミングでの処理 77void XXX() 78{ 79 /* ... */ 80 81 if( !TheStack.empty() ) 82 { 83 auto OPs = TheStack.top()->DoSomething(); 84 for( auto &OP : OPs ) //返されてきた操作要求を実施する 85 { 86 std::visit( [&TheStack]( auto &OP ){ OP.Exec(TheStack); }, OP ); 87 } 88 } 89 90 /* ... */ 91}
追記2
「いやいやそうじゃねぇよ,ごちゃごちゃ言ってないでまず最初にスタックから取り出せと」という方向のご指摘,もっともだと思います.
C++
1//何らかのタイミングでの処理 2void XXX() 3{ 4 /* ... */ 5 6 if( !TheStack.empty() ) 7 { 8 //とにかく最初にやるべきことはこれだろ,と. 9 auto TakenOut = std::move( TheStack.top() ); //変な考えは捨てて取り出すべし 10 TheStack.pop(); //(ここでpopもやるかどうかというのは以降の実装の都合次第かもだが) 11 12 //とにかく取り出したなら,そもそもここで変な問題は起きない 13 TakenOut->DoSomething(); 14 15 //--- 16 17 //(必要なら TakenOut をスタックに再度積み直すだとかいう話はあるかもだが,それは今の「問題」とは別の話) 18 if( 何らかの判断 ){ TheStack.push( std::move(TakenOut) ); } 19 } 20 21 /* ... */ 22}
