2014-08-28 2 views
5

В настоящее время я изучаю взаимодействие между полиморфными типами и операциями присваивания. Моя основная проблема заключается в том, может ли кто-то попытаться присвоить значение базового класса объекту производного класса, что вызовет проблемы.Определить назначение базового класса на ссылку, указывающую на производный класс

От this answer Я узнал, что оператор присваивания базового класса всегда скрыт неявно определенным оператором присваивания производного класса. Поэтому для назначения простой переменной неправильные типы вызовут ошибки компилятора. Однако, это не так, если назначение происходит через ссылку:

class A { public: int a; }; 
class B : public A { public: int b; }; 
int main() { 
    A a; a.a = 1; 
    B b; b.a = 2; b.b = 3; 
    // b = a; // good: won't compile 
    A& c = b; 
    c = a; // bad: inconcistent assignment 
    return b.a*10 + b.b; // returns 13 
} 

Эта форма уступки, скорее всего, приведет к inconcistent состояния объекта, однако нет никакого предупреждения компилятора и код выглядит не зол мне в первом взгляд.

Есть ли установленная идиома для обнаружения таких проблем?

Я думаю, я могу только надеяться на обнаружение во время выполнения, бросая исключение, если найду такое недопустимое назначение. Лучший подход, который я могу сейчас представить, - это определяемый пользователем оператор присваивания в базовом классе, который использует информацию типа времени выполнения, чтобы гарантировать, что this на самом деле является указателем на экземпляр базы, а не на производный класс, а затем выполняет ручную поэтапную копию. Это звучит как много накладных расходов и сильно влияет на читаемость кода. Есть что-то проще?

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

У меня есть две математические концепции, скажем ring и field. Каждое поле является кольцом, но не наоборот. Для каждого из них имеется несколько реализаций, и они имеют общие базовые классы, а именно AbstractRing и AbstractField, причем последний получен из первого. Теперь я пытаюсь реализовать легкую для записи семантику по-ссылке, основанную на std::shared_ptr. Таким образом, мой класс Ring содержит std::shared_ptr<AbstractRing> с его реализацией и кучу методов, пересылающих на него. Я хотел бы написать Field как наследующий от Ring, поэтому мне не нужно повторять эти методы. Методы, специфичные для поля, просто набрасывают указатель на AbstractField, и я бы хотел сделать это статически. Я могу убедиться, что указатель на самом деле AbstractField при строительстве, но я беспокоюсь, что кто-то назначит RingRing&, который на самом деле является Field, тем самым нарушив мой предполагаемый инвариант относительно содержащегося общего указателя.

+0

Не является ли здесь настоящей проблемой, что у вас есть не абстрактный базовый класс? –

+1

Лично я отключу конструктор и операторы присваивания для полиморфных типов. Наследственный базовый полиморфизм действительно не очень хорош в отношении типов значений. –

+0

@OliCharlesworth: Я не вижу, как. Подумайте, например. кнопка переключения, полученная с помощью кнопки, я вижу ситуации, когда базовый класс должен быть реалистичным, и проблема может возникнуть в реальном мире. Поэтому я не следовал бы за всеми принципами «все основания должны быть абстрактными». Если это не то, что вы имели в виду, то, пожалуйста, уточните, как абстрактные базовые классы могут помочь в моей ситуации. – MvG

ответ

1

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

class Ring { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Do ring assignment stuff. */ 
     return *this; 
    } 
}; 

class Field { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Trying to assign a Ring to a Field. */ 
     throw someTypeError(); 
    } 

    virtual Field& operator = (const Field& field) { 
     /* Allow assignment of complete fields. */ 
     return *this; 
    } 
}; 

Это, вероятно, самый разумный подход.

Альтернативой может быть создание класса шаблонов для ссылок, которые могут отслеживать это и просто запрещать использование основных указателей * и ссылок &. Шаблонное решение может оказаться более сложным для правильной реализации, но позволит статическую проверку типов, запрещающую downcast. Вот базовая версия, которая по крайней мере для меня корректно дает ошибку компиляции с «noDerivs (b)», являющейся источником ошибки, с использованием GCC 4.8 и флага -std = C++ 11 (для static_assert).

#include <type_traits> 

template<class T> 
struct CompleteRef { 
    T& ref; 

    template<class S> 
    CompleteRef(S& ref) : ref(ref) { 
     static_assert(std::is_same<T,S>::value, "Downcasting not allowed"); 
     } 

    T& get() const { return ref; } 
    }; 

class A { int a; }; 
class B : public A { int b; }; 

void noDerivs(CompleteRef<A> a_ref) { 
    A& a = a_ref.get(); 
} 

int main() { 
    A a; 
    B b; 
    noDerivs(a); 
    noDerivs(b); 
    return 0; 
} 

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

+0

Интересно. «Присваивание ссылочной ссылке не может быть обнаружено во время выполнения»: в C++ 11 вы можете сделать 'typeid (* this) == typeid (Ring)' внутри 'Ring :: operator = (...)' реализация , что я имел в виду. Не уверен, имеет ли виртуальный оператор лучшую или худшую производительность, пришлось бы проверить это на один день. «Это стандартный способ назначения в java». Но Java имеет ссылочную семантику, поэтому я не вижу, какое назначение вы здесь имеете в виду. Присвоение элементов массива приближается, но по-прежнему выглядит по-другому. Этот аспект Java, вероятно, не соответствует теме, но мне любопытно. – MvG

+0

@MvG Сравнение типов выполняется во время выполнения с использованием RTTI. Виртуальное назначение использует виртуальную таблицу класса и не требует сравнения. Обычно виртуальные члены предпочтительнее в C++, но я не уверен, что RTTI дает значительные накладные расходы в сравнении. Виртуальный вызов является однонаправленным указателем, тогда как RTTI может включать в себя больше (совсем не уверен). Моя java немного ржавая. Я считаю, что я имею в виду Object .equals и .clone, а не назначение, но есть аналогичная семантика. – Zoomulator

+0

Я только что понял, что написал время исполнения, когда я имел в виду время компиляции! – Zoomulator

Смежные вопросы