Ціннісні стратегії C ++ DRY


14

Щоб уникнути нетривіального копіювання C ++, пов'язаного з const, чи бувають випадки, коли const_cast буде працювати, але приватна функція const, що повертає non-const, не буде?

У пункті 3 ефективного С ++ Скотта Майєрса він припускає, що const_cast у поєднанні зі статичним складом може бути ефективним та безпечним способом уникнути повторюваного коду, наприклад

const void* Bar::bar(int i) const
{
  ...
  return variableResultingFromNonTrivialDotDotDotCode;
}
void* Bar::bar(int i)
{
  return const_cast<void*>(static_cast<const Bar*>(this)->bar(i));
}

Майєрс далі пояснює, що мати виклик функції const небезпечно.

Код нижче - зустрічний приклад, який показує:

  • всупереч пропозиціям Майєрса, іноді const_cast у поєднанні зі статичним складом є небезпечним
  • іноді функція const викликати non-const менш небезпечно
  • іноді обидва способи за допомогою const_cast приховують потенційно корисні помилки компілятора
  • уникати const_cast та мати додатковий приватний член const, який повертає non-const - інший варіант

Чи вважається хорошою практикою будь-яку із стратегій const_cast уникнення дублювання коду? Чи бажаєте ви замість цього стратегію приватного методу? Чи бувають випадки, коли const_cast буде працювати, але приватний метод не буде? Чи є інші варіанти (крім дублювання)?

Моє занепокоєння стратегіями const_cast полягає в тому, що навіть якщо код правильний під час написання, пізніше під час технічного обслуговування код може стати неправильним, а const_cast приховає корисну помилку компілятора. Здається, загальна приватна функція, як правило, безпечніша.

class Foo
{
  public:
    Foo(const LongLived& constLongLived, LongLived& mutableLongLived)
    : mConstLongLived(constLongLived), mMutableLongLived(mutableLongLived)
    {}

    // case A: we shouldn't ever be allowed to return a non-const reference to something we only have a const reference to

    // const_cast prevents a useful compiler error
    const LongLived& GetA1() const { return mConstLongLived; }
    LongLived& GetA1()
    {
      return const_cast<LongLived&>( static_cast<const Foo*>(this)->GetA1() );
    }

    /* gives useful compiler error
    LongLived& GetA2()
    {
      return mConstLongLived; // error: invalid initialization of reference of type 'LongLived&' from expression of type 'const LongLived'
    }
    const LongLived& GetA2() const { return const_cast<Foo*>(this)->GetA2(); }
    */

    // case B: imagine we are using the convention that const means thread-safe, and we would prefer to re-calculate than lock the cache, then GetB0 might be correct:

    int GetB0(int i) { return mCache.Nth(i); }
    int GetB0(int i) const { return Fibonachi().Nth(i); }

    /* gives useful compiler error
    int GetB1(int i) const { return mCache.Nth(i); } // error: passing 'const Fibonachi' as 'this' argument of 'int Fibonachi::Nth(int)' discards qualifiers
    int GetB1(int i)
    {
      return static_cast<const Foo*>(this)->GetB1(i);
    }*/

    // const_cast prevents a useful compiler error
    int GetB2(int i) { return mCache.Nth(i); }
    int GetB2(int i) const { return const_cast<Foo*>(this)->GetB2(i); }

    // case C: calling a private const member that returns non-const seems like generally the way to go

    LongLived& GetC1() { return GetC1Private(); }
    const LongLived& GetC1() const { return GetC1Private(); }

  private:
    LongLived& GetC1Private() const { /* pretend a bunch of lines of code instead of just returning a single variable*/ return mMutableLongLived; }

    const LongLived& mConstLongLived;
    LongLived& mMutableLongLived;
    Fibonachi mCache;
};

class Fibonachi
{ 
    public:
      Fibonachi()
      {
        mCache.push_back(0);
        mCache.push_back(1);
      }

      int Nth(int n) 
      {
        for (int i=mCache.size(); i <= n; ++i)
        {
            mCache.push_back(mCache[i-1] + mCache[i-2]);
        }
        return mCache[n];
      }

      int Nth(int n) const
      {
          return n < mCache.size() ? mCache[n] : -1;
      }
    private:
      std::vector<int> mCache;
};

class LongLived {};

Геттер, який щойно повертає член, коротший, ніж той, хто передає та називає іншу версію себе. Трюк призначений для більш складних функцій, коли виграш дедуплікації перевищує ризики кастингу.
Себастьян Редл

@SebastianRedl Я погоджуюся, що дублювання було б краще, якби тільки повернувся член. Будь ласка, уявіть, що це складніше, наприклад, замість повернення mConstLongLived, ми могли б викликати функцію на mConstLongLived, яка повертає посилання const, яке потім використовується для виклику іншої функції, яка повертає посилання const, до якого ми не володіємо, і маємо лише доступ до версія const. Я сподіваюся, що зрозуміло, що const_cast може видалити const з того, до чого в іншому випадку ми б не мали доступу без const.
JDiMatteo

4
Це все здається наче смішним з простих прикладів, але дублювання, пов'язане з const, з'являється в реальному коді, помилки компілятора const корисні на практиці (часто для лову дурних помилок), і я дивуюсь, що запропоноване рішення "ефективний C ++" є дивним і, здається, схильна до помилок пара закидів. Приватний член const, який повертає non-const, здається, що перевершує подвійний склад, і я хочу знати, чи щось мені не вистачає.
JDiMatteo

Відповіді:


8

При реалізації const та non-const функцій-членів, які відрізняються лише тим, чи повертається ptr / посилання const, найкращою стратегією DRY є:

  1. якщо пишете аксесуар, подумайте, чи дійсно вам потрібен цей доступ, див . відповідь cmaster та http://c2.com/cgi/wiki?AccessorsAreEvil
  2. просто дублюйте код, якщо він є тривіальним (наприклад, просто повернення члена)
  3. ніколи не використовуйте const_cast, щоб уникнути дублювання, пов'язаного з const
  4. щоб уникнути нетривіального дублювання, використовуйте приватну функцію const, що повертає non-const, який викликають і const, і non-const загальнодоступні функції

напр

public:
  LongLived& GetC1() { return GetC1Private(); }
  const LongLived& GetC1() const { return GetC1Private(); }
private:
  LongLived& GetC1Private() const { /* non-trivial DRY logic here */ }

Назвемо це функцією приватного const, що повертає неконст-шаблон .

Це найкраща стратегія уникнення дублювання прямолінійним способом, при цьому дозволяючи компілятору виконувати потенційно корисні перевірки та повідомляти про помилки, пов'язані з const.


ваші аргументи доволі переконливі, але я досить здивований, як можна отримати неконст-посилання на щось із constекземпляра (якщо тільки посилання не на щось заявлене mutable, або якщо ви не використовуєте, const_castале в обох випадках немає проблеми, для початку ). Також я не міг нічого знайти на "приватній функції const, що повертає шаблон без const" (якщо це було жартом, щоб назвати це шаблоном .... це не смішно;)
idclev 463035818

1
Ось приклад компіляції на основі коду у запитанні: ideone.com/wBE1GB . Вибачте, я не мав на увазі це як жарт, але я мав на увазі дати йому ім’я тут (напевно, це заслуговує на ім’я), і я оновив формулювання у відповіді, щоб спробувати зробити це більш зрозумілим. Минуло кілька років, як я написав це, і не пам'ятаю, чому я вважав, що приклад передачі посилання в конструкторі був релевантним.
JDiMatteo

Дякую за приклад, зараз у мене немає часу, але я обов’язково повернусь до нього. FWIW ось відповідь , який представляє той же підхід і в коментарях подібні питання були відзначені: stackoverflow.com/a/124209/4117728
idclev 463035818

1

Так, ви праві: багато програм C ++, які намагаються коректувати коректність, суворо порушують принцип DRY, і навіть приватний член, який повертає non-const, є занадто складним для комфорту.

Однак ви пропускаєте одне спостереження: дублювання коду через правильність const - це лише колись проблема, якщо ви надаєте інший код доступу до своїх учасників даних. Це саме по собі є порушенням капсулювання. Як правило, подібне дублювання коду відбувається здебільшого у простих аксесуарах (адже ви надаєте доступ уже існуючим членам, повернене значення, як правило, не є результатом обчислення).

Мій досвід полягає в тому, що хороші абстракції, як правило, не включають аксесуарів. Отже, я значною мірою уникаю цієї проблеми, визначаючи функції членів, які насправді щось роблять, а не просто забезпечують доступ до членів даних; Я намагаюся моделювати поведінку замість даних. Головний мій намір у цьому полягає в тому, щоб фактично отримати деяку абстракцію як від моїх класів, так і від їх окремих функцій-членів, а не просто використовувати мої об'єкти як контейнери даних. Але цей стиль також є досить успішним, щоб уникнути безлічі однорядкових аксесуарів const / non-const, які так часто зустрічаються у більшості кодів.


Здається, для дискусій про те, чи хороші доступні аксесуари, наприклад, дивіться дискусію на веб- сайті c2.com/cgi/wiki?AccessorsAreEvil . На практиці, незалежно від того, що ви думаєте про аксесуари, великі бази коду часто використовують їх, і якщо вони все-таки ними користуються, краще було б дотримуватися принципу DRY. Тому я думаю, що питання заслуговує на більшу відповідь, ніж на те, що ви не повинні його ставити.
JDiMatteo

1
Це, безумовно, питання, яке варто задати :-) І я навіть не заперечую, що час від часу вам потрібні аксесуари. Я просто кажу, що стиль програмування, який не заснований на аксесуарах, значно зменшує проблему. Це взагалі не вирішує проблему, але це, принаймні, досить добре для мене.
cmaster - відновити моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.