Const C ++ DRY Стратегии

14

Чтобы избежать нетривиального дублирования, связанного с константой в C ++, существуют ли случаи, когда const_cast будет работать, но частная константная функция, возвращающая non-const, не будет?

В пункте 3 « Эффективного C ++» Скотта Мейерса он предполагает, что 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 неконстантно менее опасно
  • иногда оба способа использования const_cast скрывают потенциально полезные ошибки компилятора
  • Избегание const_cast и наличие дополнительного закрытого члена 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 {};
JDiMatteo
источник
Получатель, который просто возвращает члена, короче, чем тот, который вызывает и вызывает другую версию самого себя. Уловка предназначена для более сложных функций, где выигрыш от дедупликации перевешивает риски приведения.
Себастьян Редл
@SebastianRedl Я согласен, что дублирование было бы лучше, если бы просто возвращение члена. Пожалуйста, представьте, что это более сложно, например, вместо того, чтобы возвращать mConstLongLived, мы могли бы вызвать функцию на mConstLongLived, которая возвращает ссылку на const, которая затем используется для вызова другой функции, которая возвращает ссылку на const, которой мы не владеем и у которой есть только доступ к константная версия. Я надеюсь, что суть в том, что const_cast может удалить const из того, к чему у нас иначе не было бы доступа без const.
JDiMatteo
4
Все это кажется смешным с простыми примерами, но дублирование, связанное с const, встречается в реальном коде, ошибки компилятора const полезны на практике (часто для отлова глупых ошибок), и я удивлен, что предлагаемое «эффективное C ++» решение странно и, казалось бы, подверженная ошибкам пара приведений. Частный констант, возвращающий неконста, кажется явно превосходящим двойное приведение, и я хочу знать, есть ли что-то, что я пропускаю.
JDiMatteo

Ответы:

8

При реализации константных и неконстантных функций-членов, которые различаются только в зависимости от того, является ли возвращенный ptr / reference константой, лучшая стратегия DRY заключается в следующем:

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

например

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

Давайте назовем это частной константной функцией, возвращающей неконстантный шаблон .

Это лучшая стратегия, позволяющая избегать дублирования прямым способом, при этом позволяя компилятору выполнять потенциально полезные проверки и сообщать о связанных с ними сообщениях об ошибках.

JDiMatteo
источник
ваши аргументы довольно убедительны, но я довольно озадачен тем, как вы можете получить неконстантную ссылку на что-то из constэкземпляра (если только ссылка не на что-то объявлена mutable, или если вы не используете a, const_castно в обоих случаях нет проблем с самого начала ). Кроме того, я не смог найти что - нибудь на «частную функции сопза возвращающейся неконстантную шаблон» (если она должна была быть шутки назвать модель .... оно не смешно;)
idclev 463035818
1
Вот пример компиляции, основанный на коде в вопросе: ideone.com/wBE1GB . Извините, я не имел в виду это как шутку, но я действительно хотел дать ему имя здесь (в маловероятном случае, когда оно заслуживает имени), и я обновил формулировку в ответе, чтобы попытаться прояснить это. Прошло несколько лет с тех пор, как я написал это, и я не помню, почему я думал, что пример, передающий ссылку в конструкторе, был уместен.
JDiMatteo
Спасибо за пример, у меня сейчас нет времени, но я обязательно вернусь к нему. Вот ответ, который представляет тот же подход, и в комментариях были отмечены похожие проблемы: stackoverflow.com/a/124209/4117728
idclev 463035818
1

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

Тем не менее, вы упускаете одно наблюдение: дублирование кода из-за правильности констант является проблемой, только если вы предоставляете другим членам доступ к коду данных. Это само по себе является нарушением инкапсуляции. Обычно такой тип дублирования кода встречается в основном в простых средствах доступа (в конце концов, вы передаете доступ к уже существующим элементам, возвращаемое значение обычно не является результатом вычисления).

Мой опыт показывает, что хорошие абстракции не включают в себя аксессоры. Следовательно, я в значительной степени избегаю этой проблемы, определяя функции-члены, которые действительно что-то делают, а не просто предоставляя доступ к элементам данных; Я пытаюсь смоделировать поведение вместо данных. Мое главное намерение в этом состоит в том, чтобы фактически получить некоторую абстракцию как от моих классов, так и от их отдельных функций-членов, вместо того, чтобы просто использовать мои объекты в качестве контейнеров данных. Но этот стиль также весьма успешен в том, чтобы избежать множества постоянных / неконстантных повторяющихся однострочных аксессоров, которые так распространены в большинстве кодов.

cmaster - восстановить монику
источник
Похоже для обсуждения, хороши ли средства доступа, например, смотрите обсуждение на c2.com/cgi/wiki?AccessorsAreEvil . На практике, независимо от того, что вы думаете о средствах доступа, их часто используют большие базы кода, и если они их используют, было бы лучше придерживаться принципа СУХОЙ. Поэтому я думаю, что вопрос заслуживает большего ответа, чем то, что вы не должны его задавать.
JDiMatteo
1
Это, безусловно, вопрос, который стоит задать :-) И я даже не буду отрицать, что время от времени вам нужны средства доступа. Я просто говорю, что стиль программирования, не основанный на методах доступа, значительно уменьшает проблему. Это не решает проблему в целом, но это по крайней мере достаточно хорошо для меня.
cmaster - восстановить монику