Как сгенерировать неконстантный метод из константного метода?

Стремясь к константной корректности, я часто пишу такой код

class Bar;

class Foo {
public:
  const Bar* bar() const { /* code that gets a Bar somewhere */ }

  Bar* bar() {
    return const_cast< Bar* >(
      static_cast< const Foo* >(this)->bar());
  }
};

для множества таких методов, как bar(). Написание этих неконстантных методов, которые вызывают константные методы вручную, утомительно; кроме того, я чувствую, что повторяюсь, и мне становится плохо.

Что я могу сделать, чтобы облегчить эту задачу? (Макросы и генераторы кода не допускаются.)

Изменить: помимо решения litb мне также нравится мое собственное. :)


person Tobias    schedule 26.08.2009    source источник
comment
Вы не ясно дали понять, что делаете на самом деле. Смотрите мой ответ на ваш ответ на мой ответ (и комментарии некоторых других людей) ниже.   -  person JDonner    schedule 28.08.2009
comment
Взгляните на Скотт Мейерс, Эффективный C ++, 3-е изд. - Пункт 3 (используйте const): [p23] Избегайте дублирования в константных и неконстантных функциях-членах. [p26] Когда константные и неконстантные членские функции имеют [одинаковые] реализации, дублирования кода можно избежать, если неконстантная версия вызывает константную версию.   -  person Tobias    schedule 28.08.2009


Ответы (11)


Другой способ - написать шаблон, который вызывает функцию (используя CRTP) и наследует ее.

template<typename D>
struct const_forward {
protected:
  // forbid deletion through a base-class ptr
  ~const_forward() { }

  template<typename R, R const*(D::*pf)()const>
  R *use_const() {
    return const_cast<R *>( (static_cast<D const*>(this)->*pf)() );
  }

  template<typename R, R const&(D::*pf)()const>
  R &use_const() {
    return const_cast<R &>( (static_cast<D const*>(this)->*pf)() );
  }
};

class Bar;

class Foo : public const_forward<Foo> {
public:
  const Bar* bar() const { /* code that gets a Bar somewhere */ }
  Bar* bar() { return use_const<Bar, &Foo::bar>(); }
};

Обратите внимание, что при вызове не происходит потери производительности: поскольку указатель на член передается как параметр шаблона, вызов может быть встроен как обычно.

person Johannes Schaub - litb    schedule 26.08.2009
comment
Интересно, что это похоже на решение, которое я придумал сам, немного поразмыслив :) Два комментария: 1. Тот факт, что каждый класс должен наследовать от const_forward, несколько прискорбен (и я лично предпочел бы здесь защищенное наследование). 2. Вы проверяли это на msvc? У меня было довольно много проблем с тем, что msvc определял правильный указатель на функцию-член, если подписи различаются только на const. Тем не менее, ИМХО пока что лучшее решение! - person Tobias; 28.08.2009
comment
К сожалению, если вы наследуете protected, вам нужно сделать базу другом производного класса, чтобы база видела наследование, или вы используете приведение C-Style для приведения из base this к производному this (приведение в стиле c будет по-прежнему определено - даже если задействовано множественное наследование, и не будет заботиться о видимости). Но я считал гипс в стиле «c» слишком опасным. Я тестировал это на Comeau и GCC :) - person Johannes Schaub - litb; 28.08.2009
comment
Вы можете использовать сдерживание и сделать const_forward членом. Но тогда вам нужно передать указатель this в какой-то момент или при всех вызовах use_const. Мне это не понравилось :) - person Johannes Schaub - litb; 28.08.2009
comment
Ооо! Я не знал, что стандарт разрешает только понижающие преобразования в стиле C от защищенных базовых классов. Это снова одна из тех особенностей C ++, которые заставляют меня плакать Почему Бьярн ?! Что ж, gcc и msvc с радостью игнорируют эту ошибку. - person Tobias; 28.08.2009
comment
Нет, конечно, он также позволяет преобразовывать приведение в стиле C в приведение из общедоступных баз. Но в отличие от static_cast, приведение в стиле c позволяет выполнять приведение из защищенных / частных баз и в защищенные / частные базы. И в отличие от reinrerpret_cast, приведение с использованием приведения в стиле c будет совершенно корректным, и при необходимости будут внесены корректировки адреса. - person Johannes Schaub - litb; 28.08.2009
comment
Мне тоже не нравятся решения, требующие передачи указателя this :) Что мне нравится в вашем решении, так это то, что вы передаете указатель как аргумент шаблона. Что мне не нравится (и что я предпочитаю в своем решении :), так это то, что это означает, что вам нужно снова записать типы всех параметров на сайте вызова. Я пытался подумать об этом, но не смог найти способ указать только метод в качестве параметра шаблона, но не типы. Вы представляете, как это можно сделать? - person Tobias; 28.08.2009
comment
Что ж, если вы можете использовать макрос и иметь __typeof__ или decltype или подобное расширение c ++ 0x /, вы можете сделать #define FN(X) decltype(getC(&X)), X, а затем вы можете сделать use_const<Bar, FN(Foo::Bar)>(). getC будет шаблоном функции, например template<typename R, typename A1, typename C> R(C::*getC(R(C::*)(A1)const))(A1)const; и дальнейшие перегрузки для каждого другого счетчика параметров .... Но я не знаю более простого способа решить эту проблему. Тем более, что я не знаю без макросов и по-прежнему лаконичных способов. И я не знаю, как это сделать без _7 _ / _ 8_ - person Johannes Schaub - litb; 28.08.2009
comment
Что ж, проблема с шаблоном функции, подобным getC, заключается в том, что в msvc есть ошибка, из-за которой они не могут разрешить перегрузку в этом случае, если перегрузка просто отличается по типу cv и которую, как они заявляют, невозможно исправить. Тогда посмотрите мое решение, вот где я это обнаружил :( - person Tobias; 31.08.2009

Используйте следующий трюк:


class Bar;
class Foo {
public:  
  Bar* bar() { 
    // non-const case
    /* code that does something */ 
  }  
  const Bar* bar() const {    
      return This().bar();  // use non-const case
   }

private:
  //trick: const method returns non-const reference
  Foo & This() const { return const_cast<Foo &>(*this); } 
};

Обратите внимание, что можно использовать уникальную функцию This для любых константных / неконстантных функций.

Альтернативное решение без static_cast (но я предпочитаю первое):

class Bar;
class Foo {
public:  
  const Bar* bar() const { /* code that does something */ }  
  Bar* bar() { return const_cast<Bar*>(cthis().bar()); } // use const case
private:
  const Foo & cthis() const { return *this; } 
};
person Alexey Malistov    schedule 26.08.2009
comment
-1. Если вы вызываете bar () const для объекта действительно const, он вызовет This () const, попытается удалить константу, и вы получите неопределенное поведение. - person Tadeusz Kopec; 26.08.2009
comment
Вопрос OP подразумевает, что неконстантная функциональная панель ничего не меняет. - person Alexey Malistov; 26.08.2009
comment
Окей, прости. Я думал, что const_cast достаточно, чтобы вызвать UB, но стандарт говорит, что UB после модификации. - person Tadeusz Kopec; 26.08.2009
comment
+1 за второе решение. const_cast в этом случае довольно безопасен, потому что он будет применен к изначально неконстантному объекту. - person Kirill V. Lyadvinsky; 26.08.2009
comment
Задача программиста - выяснить, изменяет ли код объекты некорректно или нет. Если это приемлемо, то зачем стремиться к константной корректности для начала, а потом отказываться от этого? - person Steve Jessop; 26.08.2009
comment
Первый по-прежнему опасен. Изолированная функция должна быть безопасной, независимой от других перегрузок. Это решение делает версию с константой небезопасной, потому что это зависит от того факта, что неконстантная версия не записывает в объект. Рекомендую: всегда использовать вторую версию. - person Johannes Schaub - litb; 27.08.2009
comment
Второй основан на том факте, что версия с константой возвращает изменяемый объект, если он является изменяемым. Mutatis mutandem, это тоже небезопасно. На самом деле все зависит от того, насколько безопасно, по вашему мнению, достаточно безопасно, что зависит от сложности кода, на непроверенное поведение константы которого вы полагаетесь. - person Steve Jessop; 03.09.2009

Вы можете сделать что-то вроде этого:

class Bar;

class Foo {
public:
  const Bar* bar() const { return getBar(); }

  Bar* bar() {
   return getBar();
  }

  private:
    Bar* getBar() const {/* Actual code */ return NULL;}
};
person Naveen    schedule 26.08.2009
comment
Итак, вы в конечном итоге пишете три функции вместо двух? Не лучшее решение, ИМХО. - person ; 26.08.2009
comment
Я пытался избежать повторения кода или использовать const_cast. Первые две функции не делают ничего, кроме вызова внутренней вспомогательной функции. - person Naveen; 26.08.2009
comment
@Neil: Здесь два противоречащих друг другу требования. Желательно, чтобы константный член возвращал константный указатель. Результат кода, который что-то делает, должен быть изменяемым, поэтому он должен возвращать неконстантный указатель. Чтобы уловить оба этих требования, самый простой вариант - иметь третью вспомогательную функцию. - person Richard Corden; 26.08.2009
comment
@Neil: удаление дублирования кода обычно решается введением новой функции с общим кодом, не так ли? - person Tadeusz Kopec; 26.08.2009
comment
Bar* getBar() const не может быть скомпилирован, если он возвращает указатель на переменную-член. - person Kirill V. Lyadvinsky; 26.08.2009
comment
@ JIa3ep: Я согласен, что он не будет компилироваться. Но в первую очередь это не должен быть константный метод, если это так. Итак, я предположил, что он возвращает копию указателя. - person Naveen; 26.08.2009
comment
OP сказал где-то в комментариях: Нет, иногда он возвращает участника ‹...› - person Kirill V. Lyadvinsky; 26.08.2009

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

person Community    schedule 26.08.2009
comment
Есть ли что-нибудь подозрительное в векторах iter begin () и const_iter begin () const? Если бы не было того и другого, возникла бы проблема. - person Tadeusz Kopec; 26.08.2009
comment
+1 за изменение дизайна, -1 за изменение переменных - person Grant Peters; 26.08.2009
comment
@tkopec две функции begin () на самом деле возвращают совершенно разные типы, чего нет в коде OP - person ; 26.08.2009
comment
Я согласен с Нилом в том, что «изменчивый» - элегантное решение этой проблемы. В этом случае вообще не будет проблемы с дублированием кода. Что, если в классе есть несколько геттеров / сеттеров с парами const-nonconst, необходимыми для каждого? Сколько общих функций нужно будет заменить? - person Abhay; 26.08.2009
comment
Псевдо-примером может быть class Car, у которого есть tires. Я хочу иметь возможность myConstCar.tires().spare().isInflated() и myCar.tires().spare().inflate(). Я хочу избежать добавления Car::isSpareTireInflated() и Car::inflateSpareTire(), поскольку это приводит к резкому увеличению количества методов Car. - person Tobias; 26.08.2009
comment
mutable там, где это нужно, это хорошо. Я всегда делаю вещи, которые не меняют значение объекта mutable - даже если мне не нужно изменять его константными функциями-членами. - person Johannes Schaub - litb; 27.08.2009

Я тоже испытывал эту боль раньше - по сути, вы пытаетесь сообщить компилятору, что константность «распространяется» через bar(). К сожалению, насколько мне известно, это невозможно сделать автоматически ... вам просто нужно написать вторую версию функции вручную.

person Martin B    schedule 26.08.2009

К вашему сведению. Код, опубликованный OP, является предпочтительным методом, приведенным в «Эффективном C ++ - третье издание» Скотта Мейерса. См. Пункт № 3.

person mocj    schedule 27.08.2009

Ответ я придумал сам, немного поразмыслив. Однако я думаю, что могу улучшить его, используя идеи из ответа litb, который я опубликую позже. Итак, мое решение пока выглядит так:

class ConstOverloadAdapter {
protected:

  // methods returning pointers 

  template< 
    typename R, 
    typename T >
  R* ConstOverload(
    const R* (T::*mf)(void) const) {
      return const_cast< R* >(
        (static_cast< const T* >(this)->*mf)());
    }

  // and similar templates for methods with parameters 
  // and/or returning references or void
};

class Bar;

class Foo : public ConstOverloadAdapter {
public:
  const Bar* bar() const { 
    /* implementation */ }

  Bar* bar(void* = 0) {  // the dummy void* is only needed for msvc
                         // since it cannot distinguish method overloads
                         // based on cv-type. Not needed for gcc or comeau.
    return ConstOverload(&Foo::bar); }
};
person Tobias    schedule 28.08.2009

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

  1. Когда "this" не является константой, вызывающий объект может безопасно изменить ссылочный номер возвращаемого вами указателя.
  2. Когда "this" является константным, любая работа, которую вы выполняете, не выполняет никаких неконстантных операций с "this".

Если версия с константой вызывает неконстантную, вы не получите (2). Если неконстантная версия вызывает константную и const_cast результат, то вы не получите (1). Например, предположим, что Bar на самом деле char, и код, который вы пишете, возвращает (в некоторых случаях) строковый литерал. Это будет компилироваться (и -Wwrite-strings не выдает предупреждения), но ваш вызывающий объект получает неконстантный указатель на строковый литерал. Это противоречит «вы предпочитаете, чтобы константная корректность проверялась компилятором».

Если они оба вызывают вспомогательную функцию-член Bar *getBar() const, вы получаете и (1), и (2). Но если можно написать эту вспомогательную функцию, то зачем вы вообще возитесь с константными и неконстантными версиями, когда совершенно нормально изменять Bar, возвращаемый из const Foo? Иногда, возможно, некоторые детали реализации означают, что вы реализуете интерфейс с двумя средствами доступа, даже если вам нужен только один. В противном случае либо помощник не может быть записан, либо две функции могут быть заменены одним единственным помощником.

Пока размер кода не вызывает беспокойства, я думаю, что лучший способ достичь и (1), и (2) - это заставить компилятор действительно учитывать оба случая:

struct Bar { int a; };
struct Foo {
          Bar *bar()       { return getBar<Bar>(this); }
    const Bar *bar() const { return getBar<const Bar>(this); }

    Bar *bar2() const { return getBar<Bar>(this); } // doesn't compile. Good.
    Bar *bar3() const { return getBar<const Bar>(this); } // likewise

    private:
    template <typename B, typename F>
    static B *getBar(F *self) {
        // non-trivial code, which can safely call other functions with 
        // const/non-const overloads, and we don't have to manually figure out
        // whether it's safe to const_cast the result.
        return &self->myBar;
    }
    Bar myBar;
};

Если код тривиален, например operator[], который обращается к некоторому массиву, принадлежащему объекту, то я бы просто продублировал код. В какой-то момент вышеупомянутый шаблон функции требует меньше усилий по кодированию, чем дублирование, и в этот момент используйте шаблон.

Я думаю, что подход const_cast, хотя и умный и, казалось бы, стандартный, просто бесполезен, потому что он предпочитает краткость проверенной компилятором корректности const. Если код в методе тривиален, вы можете его продублировать. Если это нетривиально, то вам или специалисту по сопровождению кода нелегко убедиться, что const_cast действительно действителен.

person Steve Jessop    schedule 26.08.2009

Ваш код подразумевает, что const bar () на самом деле создает и возвращает новый Bar, и я нахожу это странным, если вы так много делаете.

Для меня более серьезная проблема заключается в том, что константность в функции-члене ограничена ссылочными и не-указательными элементами. В тот момент, когда у вас есть (не константные) члены-указатели, константная функция может изменить их, даже если в поле она утверждает, что является константой.

Если ваш экземпляр Bar является переменной-членом, рассмотрите возможность возврата вместо этого ссылки.

person Will    schedule 26.08.2009
comment
Нет, иногда он возвращает член, иногда он запрашивает требуемый член у другого объекта. Что касается проблемы ссылки и указателя: ИМХО, это вопрос стиля, и это не меняет проблемы. - person Tobias; 26.08.2009

Влияет ли постоянство Bar на постоянство вашего Foo? Или вы просто делаете это потому, что нет возможности отличить версии const Bar * от Bar *? В последнем случае у меня будет только один bar (), который вернет Bar *, и с ним покончено. В конце концов, что-то, что принимает константу Bar *, в любом случае вполне может принять Bar *. Не забывайте, что постоянство методов bar () касается Foo, а не Bar.

Кроме того, если вы предлагаете как константную, так и неконстантную Bar * и счастливы, что Foo не является константой, волей-неволей, то константность вашего Foo (и, следовательно, методов bar ()), очевидно, не так важна , вы просто заполняете константные и неконстантные версии. В этом случае, если вы разрешите неконстантную версию, снова просто предложите / only / эту, потребители const с радостью примут неконстантную. Просто то, как у вас есть const и non-const, похоже, не имеет большого значения для программы.

Если вы хотите, чтобы объекты const Foo могли возвращать как константные, так и неконстантные Bar s, просто сделайте bar () const и верните неконстантный Bar.

Я не знаю, мне нужно было бы увидеть больше, чтобы дать реальный ответ.

Однако подумайте, какие константы действительно связаны и нужны ли вашей программе как const Foo, так и неконстантные Foo. Допуская все возможности волей-неволей, кажется, что вы не продумали, что требует программа.

person JDonner    schedule 26.08.2009
comment
Я полностью не согласен. Причина такова: если this Foo имеет значение const, я не могу вернуть ссылку на неконстантный элемент Bar (который затем может косвенно изменить своего родителя). Однако, если this Foo не является константным, я могу вернуть неконстантный элемент Bar. - person Tobias; 28.08.2009
comment
Нет, Бар / не является участником /. См. Его комментарий «... код, который где-то получает Bar ..». Так что это не влияет на постоянство Foo. - person JDonner; 28.08.2009
comment
... о, ты ОП. Что ж, тогда дайте больше кода и сделайте его понятнее. - person JDonner; 28.08.2009

person    schedule
comment
Спасибо за конкретный пример. Кроме того, не могли бы уточнить, как ваш ответ соотносится с моим вопросом? - person Tobias; 26.08.2009