كيف يمكنني إزالة تكرار التعليمات البرمجية بين وظائف الأعضاء المتشابهة وغير الثابتة؟

StackOverflow https://stackoverflow.com/questions/123758

سؤال

لنفترض أن لدي ما يلي class X حيث أريد إعادة الوصول إلى عضو داخلي:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

وظائف العضوين X::Z() و X::Z() const لها رمز متطابق داخل الأقواس.هذا رمز مكرر ويمكن أن يسبب مشاكل في الصيانة للوظائف الطويلة ذات المنطق المعقد.

هل هناك طريقة لتجنب تكرار هذا الكود؟

هل كانت مفيدة؟

المحلول 2

نعم، من الممكن تجنب تكرار الكود.تحتاج إلى استخدام وظيفة العضو const للحصول على المنطق وجعل وظيفة العضو غير const تستدعي وظيفة العضو const وتعيد تحويل قيمة الإرجاع إلى مرجع غير const (أو مؤشر إذا كانت الوظائف ترجع مؤشرًا):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& Z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& Z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).Z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.Z(index) );
   }
 #endif
};

ملحوظة: من المهم أن تفعل لا ضع المنطق في الدالة غير const واطلب من الدالة const استدعاء الدالة غير const - قد يؤدي ذلك إلى سلوك غير محدد.والسبب هو أن مثيل الفئة الثابتة يتم تحويله كمثيل غير ثابت.قد تقوم وظيفة العضو غير الثابت بتعديل الفئة عن طريق الخطأ، مما سيؤدي إلى حدوث سلوك غير محدد في الحالات القياسية لـ C++.

نصائح أخرى

للحصول على شرح مفصل، يرجى الاطلاع على العنوان "تجنب الازدواجية في const وغيرconst وظيفة العضو" في ص.23، في البند 3 "الاستخدام const كلما أمكن ذلك" في فعالة C ++, ، طبعة ثلاثية الأبعاد بقلم سكوت مايرز، ISBN-13:9780321334879.

alt text

إليك حل مايرز (مبسط):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

قد يكون الأمران واستدعاء الوظيفة قبيحين ولكنه صحيح.مايرز لديه تفسير شامل لماذا.

أعتقد أنه يمكن تحسين حل Scott Meyers في C++ 11 باستخدام وظيفة مساعد مؤقت.وهذا يجعل النية أكثر وضوحًا ويمكن إعادة استخدامها للعديد من الحروف الأخرى.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

يمكن استخدام هذه الوظيفة المساعدة بالطريقة التالية.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

الوسيطة الأولى هي دائمًا هذا المؤشر.والثاني هو المؤشر إلى وظيفة العضو المراد الاتصال بها.بعد ذلك يمكن تمرير كمية عشوائية من الوسائط الإضافية بحيث يمكن إعادة توجيهها إلى الوظيفة.هذا يحتاج إلى C++ 11 بسبب القوالب المتنوعة.

أكثر إسهابا قليلا من مايرز، ولكن قد أفعل هذا:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

تحتوي الطريقة الخاصة على خاصية غير مرغوب فيها وهي أنها تُرجع Z& غير ثابتة لمثيل ثابت، وهذا هو سبب كونها خاصة.قد تؤدي الطرق الخاصة إلى كسر متغيرات الواجهة الخارجية (في هذه الحالة يكون المتغير المطلوب هو "لا يمكن تعديل كائن const عبر المراجع التي يتم الحصول عليها من خلاله إلى الكائنات التي لديه-a").

لاحظ أن التعليقات جزء من النمط - تحدد واجهة _getZ أنه من غير الصحيح أبدًا الاتصال به (بصرف النظر عن أدوات الوصول، بوضوح):لا توجد فائدة يمكن تصورها للقيام بذلك على أي حال، لأنه يتطلب حرفًا واحدًا إضافيًا للكتابة ولن يؤدي إلى كود أصغر أو أسرع.استدعاء الطريقة يعادل استدعاء أحد الموصلات باستخدام const_cast، ولن ترغب في القيام بذلك أيضًا.إذا كنت قلقًا بشأن جعل الأخطاء واضحة (وهذا هدف عادل)، فقم بتسميتها const_cast_getZ بدلاً من _getZ.

بالمناسبة، أنا أقدر حل مايرز.ليس لدي أي اعتراض فلسفي على ذلك.على الرغم من ذلك، شخصيًا، أفضل قدرًا بسيطًا من التكرار المتحكم فيه، والطريقة الخاصة التي يجب استدعاؤها فقط في ظروف معينة خاضعة لرقابة مشددة، على الطريقة التي تبدو مثل ضجيج الخط.اختر سمك والتزم به.

[يحرر:لقد أشار Kevin بحق إلى أن _getZ قد يرغب في استدعاء طريقة أخرى (على سبيل المثال، generatorZ) وهي متخصصة في نفس طريقة getZ.في هذه الحالة، قد يرى _getZ const Z& وسيتعين عليه const_cast قبل العودة.لا يزال هذا آمنًا، نظرًا لأن الملحق المعياري يراقب كل شيء، ولكن ليس من الواضح تمامًا أنه آمن.علاوة على ذلك، إذا قمت بذلك ثم قمت لاحقًا بتغيير generatorZ لإرجاع const دائمًا، فستحتاج أيضًا إلى تغيير getZ لإرجاع const دائمًا، لكن المترجم لن يخبرك بذلك.

هذه النقطة الأخيرة حول المترجم تنطبق أيضًا على النمط الموصى به من قبل مايرز، ولكن النقطة الأولى حول const_cast غير الواضحة ليست كذلك.لذا، بشكل عام، أعتقد أنه إذا تبين أن _getZ يحتاج إلى const_cast لقيمة الإرجاع الخاصة به، فإن هذا النمط يفقد الكثير من قيمته مقارنة بمايرز.نظرًا لأنه يعاني أيضًا من عيوب مقارنة بمايرز، أعتقد أنني سأتحول إليه في هذه الحالة.تعد إعادة البناء من واحدة إلى أخرى أمرًا سهلاً -- فهي لا تؤثر على أي تعليمات برمجية صالحة أخرى في الفصل، نظرًا لأن التعليمات البرمجية غير الصالحة والنموذج المعياري يستدعيان _getZ فقط.]

قام C++ 17 بتحديث أفضل إجابة لهذا السؤال:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

وهذا له المزايا التي:

  • من الواضح ما يحدث
  • يحتوي على الحد الأدنى من حمل التعليمات البرمجية - فهو يتناسب مع سطر واحد
  • من الصعب أن نخطئ (لا يمكن إلا أن نتخلص منه volatile بالصدفة ولكن volatile هو مؤهل نادر)

إذا كنت تريد اتباع مسار الاستقطاع الكامل، فيمكن تحقيق ذلك من خلال وجود وظيفة مساعدة

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
void as_mutable(T const &&) = delete;

الآن لا يمكنك حتى الفوضى volatile, ، ويبدو الاستخدام

T & f() {
    return as_mutable(std::as_const(*this).f());
}

سؤال جميل وإجابات جميلة.لدي حل آخر لا يستخدم الجبائر:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

ومع ذلك، فإن فيه قبح الحاجة إلى عضو ثابت والحاجة إلى استخدام instance المتغير بداخله.

لم أفكر في كل الآثار (السلبية) المحتملة لهذا الحل.واسمحوا لي أن أعرف إذا كان هناك أي شيء.

يمكنك أيضًا حل هذه المشكلة باستخدام القوالب.هذا الحل قبيح بعض الشيء (ولكن القبح مخفي في ملف .cpp) ولكنه يوفر فحصًا للمترجم من الثبات، ولا يوجد تكرار للتعليمات البرمجية.

ملف .h:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

ملف .cpp:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

العيب الرئيسي الذي يمكنني رؤيته هو أنه نظرًا لأن كل التنفيذ المعقد لهذه الطريقة يتم في وظيفة عامة، فأنت بحاجة إما إلى الحصول على أعضاء X باستخدام الأساليب العامة مثل GetVector() أعلاه (والتي يجب دائمًا أن يكون هناك إصدار const وnon-const) أو يمكنك جعل هذه الوظيفة صديقًا.لكنني لا أحب الأصدقاء.

[يحرر:تمت إزالة التضمين غير الضروري لـ cstdio الذي تمت إضافته أثناء الاختبار.]

ماذا عن نقل المنطق إلى طريقة خاصة والقيام فقط بأشياء "الحصول على المرجع والعودة" داخل الحروف؟في الواقع، سأكون في حيرة من أمري بشأن القوالب الثابتة والثابتة داخل دالة getter البسيطة، وسأعتبر ذلك قبيحًا باستثناء الظروف النادرة للغاية!

هل يعد استخدام المعالج المسبق غشًا؟

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

إنها ليست خيالية مثل القوالب أو القوالب، ولكنها تجعل نيتك ("هاتين الوظيفتين متطابقتين") واضحة جدًا.

عادة، وظائف الأعضاء التي تحتاج إلى إصدارات const وغير const هي الحروف والأدوات.في معظم الأحيان تكون عبارة عن سطر واحد، لذا لا يمثل تكرار التعليمات البرمجية مشكلة.

لقد فعلت هذا من أجل صديق برر بحق استخدام const_cast...لا أعرف عن ذلك ربما كنت سأفعل شيئًا كهذا (ليس أنيقًا حقًا):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}

أقترح قالب وظيفة ثابتة للمساعد الخاص، مثل هذا:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};

لأولئك (مثلي) الذين

  • يستخدم ج++17
  • تريد إضافة أقل كمية من القالب/ التكرار و
  • لا تمانع في استخدام وحدات الماكرو (أثناء انتظار الفئات الفوقية...)،

هنا لقطة أخرى:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T>                                                \
    auto func(T&&... a) -> typename NonConst<decltype(func(a...))>::type {  \
        return const_cast<decltype(func(a...))>(                            \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

إنه في الأساس مزيج من الإجابات المقدمة من @Pait و@DavidStone و@sh1.ما يضيفه إلى الجدول هو أنك تفلت من سطر إضافي واحد فقط من التعليمات البرمجية الذي يقوم ببساطة بتسمية الوظيفة (ولكن لا توجد وسيطة أو تكرار نوع الإرجاع):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

ملحوظة:فشل gcc في تجميع هذا قبل الإصدار 8.1، و clang-5 وما فوق وكذلك MSVC-19 سعداء (وفقًا لـ مستكشف المترجم).

من المفاجئ بالنسبة لي أن هناك العديد من الإجابات المختلفة، ولكن جميعها تقريبًا تعتمد على سحر القالب الثقيل.تعتبر القوالب قوية، ولكن في بعض الأحيان تتفوق عليها وحدات الماكرو في الإيجاز.غالبًا ما يتم تحقيق أقصى قدر من التنوع من خلال الجمع بين الاثنين.

لقد كتبت ماكرو FROM_CONST_OVERLOAD() والتي يمكن وضعها في الدالة غير const لاستدعاء الدالة const.

مثال على الاستخدام:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

تنفيذ بسيط وقابل لإعادة الاستخدام:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

توضيح:

كما هو منشور في العديد من الإجابات، فإن النمط النموذجي لتجنب تكرار التعليمات البرمجية في وظيفة عضو غير ثابت هو:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

يمكن تجنب الكثير من هذا النموذج باستخدام الاستدلال النوعي.أولاً، const_cast يمكن أن تكون مغلفة في WithoutConst(), ، الذي يستنتج نوع الوسيطة ويزيل المؤهل const.ثانيا، يمكن استخدام نهج مماثل في WithConst() لتأهيل ثابت this المؤشر، والذي يتيح استدعاء الأسلوب const-overloaded.

والباقي عبارة عن ماكرو بسيط يبدأ المكالمة بالمؤهل بشكل صحيح this-> ويزيل const من النتيجة.نظرًا لأن التعبير المستخدم في الماكرو يكون دائمًا عبارة عن استدعاء دالة بسيط مع وسيطات مُعاد توجيهها 1:1، فإن عيوب وحدات الماكرو مثل التقييم المتعدد لا تظهر.القطع الناقص و __VA_ARGS__ يمكن استخدامها أيضًا، ولكن لا يجب أن تكون هناك حاجة إليها لأن الفواصل (كفواصل الوسيطات) تظهر بين قوسين.

هذا النهج لديه العديد من الفوائد:

  • بناء الجملة البسيط والطبيعي - ما عليك سوى إنهاء المكالمة FROM_CONST_OVERLOAD( )
  • لا توجد وظيفة عضو إضافية مطلوبة
  • متوافق مع سي++98
  • تنفيذ بسيط، بدون برمجة قالبية وتبعيات صفرية
  • توسع:يمكن إضافة علاقات ثابتة أخرى (مثل const_iterator, std::shared_ptr<const T>, ، إلخ.).لهذا، ببساطة الزائد WithoutConst() للأنواع المقابلة.

محددات:تم تحسين هذا الحل للسيناريوهات التي يؤدي فيها التحميل الزائد غير الثابت تمامًا مثل التحميل الزائد الثابت، بحيث يمكن إعادة توجيه الوسائط 1:1.إذا كان منطقك مختلفًا ولا تتصل بإصدار const عبر this->Method(args), ، يمكنك التفكير في طرق أخرى.

هذه المادة دج يعرض طريقة لاستخدام تخصص القالب الذي لا يتطلب منك استخدام const_cast.بالنسبة لمثل هذه الوظيفة البسيطة، ليست هناك حاجة إليها حقًا.

يستخدم Boost::any_cast (في مرحلة ما، لم يعد كذلك) const_cast من إصدار const الذي يستدعي الإصدار غير const لتجنب الازدواجية.لا يمكنك فرض دلالات ثابتة على الإصدار غير الثابت رغم ذلك، يجب أن تكون كذلك جداً حذرا مع ذلك.

في النهاية بعض الازدواجية في التعليمات البرمجية يكون حسنًا طالما أن المقتطفين موجودان مباشرة فوق بعضهما البعض.

للإضافة إلى الحل المقدم من jwfearn وkevin، إليك الحل المقابل عندما تقوم الدالة بإرجاع Shared_ptr:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};

لم أجد ما كنت أبحث عنه، لذا قمت بجمع قطعتين مني...

هذه الطريقة كثيرة الكلام، ولكنها تتمتع بميزة التعامل مع العديد من الأساليب المثقلة بنفس الاسم (ونوع الإرجاع) مرة واحدة:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

إذا كان لديك واحد فقط const طريقة لكل اسم، ولكن لا يزال هناك الكثير من الطرق لتكرارها، فقد تفضل هذا:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

لسوء الحظ، ينهار هذا الأمر بمجرد البدء في تحميل الاسم بشكل زائد (يبدو أن قائمة وسائط وسيطة مؤشر الوظيفة لم يتم حلها في تلك المرحلة، لذلك لا يمكن العثور على تطابق لوسيطة الوظيفة).على الرغم من أنه يمكنك تصميم طريقك للخروج من ذلك أيضًا:

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

لكن الحجج المرجعية إلى const فشل الأسلوب في المطابقة مع وسيطات القيمة الثانوية الظاهرة للقالب وينقطع. غير متأكد من السبب.هذا هو السبب.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top