Как сгенерировать предупреждение / ошибку компилятора при нарезке объекта

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

Примечание:

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

2. Я предпочитаю параметр компилятора (VC++) для отключения или включения нарезки объектов, если таковой имеется.

class Base{};
class Derived: public Base{};

void Func(Base)
{

}

//void Func(Derived)
//{
//
//}

//main
Func(Derived());

Здесь, если я закомментирую вторую функцию, будет вызвана первая функция - и компилятор (как VC++, так и Gcc) чувствует себя комфортно с этим.

Это стандарт C++? и могу ли я попросить компилятор (VC++) выдавать предупреждение при встрече с таким кодом?

Огромное спасибо!!!

Редактировать:

Большое спасибо за вашу помощь!

Я не могу найти параметр компилятора, чтобы выдать ошибку / предупреждение - я даже разместил это на форуме MSDN для консультанта по компилятору VC++ без ответа. Так что я боюсь, что ни gcc, ни vc ++ не реализовали эту функцию.

Поэтому лучшим решением на данный момент будет добавление конструктора, который принимает производные классы в качестве параметра.

Редактировать

Я отправил отзыв в MS и надеюсь, что они скоро его исправят:

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=421579

-Байян

Ответов (8)

Решение

Если вы можете изменить базовый класс, вы можете сделать что-то вроде:

class Base
{
public:
// not implemented will cause a link error
    Base(const Derived &d);
    const Base &operator=(const Derived &rhs);
};

В зависимости от вашего компилятора, который должен предоставить вам единицу перевода и, возможно, функцию, в которой происходит нарезка.

class Derived: public Base{};

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

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

class Base
{
private:   // To force a compile error for non-friends (thanks bk1e)
// Not implemented, so will cause a link error for friends
    template<typename T> Base(T const& d);
    template<typename T> Base const& operator=(T const& rhs);

public:
// You now need to provide a copy ctor and assignment operator for Base
    Base(Base const& d) { /* Initialise *this from d */ }
    Base const& operator=(Base const& rhs) { /* Copy d to *this */ }
};

Хотя это снижает объем необходимой работы, при таком подходе вам все равно придется возиться с каждым базовым классом, чтобы защитить его. Кроме того , это может вызвать проблемы , если есть законные преобразования из Base к SomeOtherClass этой службе operator Base() члена SomeOtherClass . (В этом случае можно использовать более сложное решение boost::disable_if<is_same<T, SomeOtherClass> > .) В любом случае вам следует удалить этот код, как только вы определили все экземпляры нарезки объектов.

Для разработчиков компиляторов во всем мире: тестирование нарезки объектов - это определенно то, для чего стоит создавать (необязательные) предупреждения! Я не могу придумать ни одного случая, когда было бы желаемое поведение, и это очень часто встречается в коде C++ новичков.

[РЕДАКТИРОВАТЬ 27/3/2015:] Как указал Мэтт Макнаб, на самом деле вам не нужно явно объявлять конструктор копирования и оператор присваивания, как я сделал выше, поскольку они все равно будут неявно объявляться компилятором. В стандарте C++ 2003 года это прямо упоминается в сноске 106 под 12.8 / 2:

Поскольку конструктор шаблона никогда не является конструктором копирования, наличие такого шаблона не подавляет неявное объявление конструктора копирования. Конструкторы шаблонов участвуют в разрешении перегрузки с другими конструкторами, включая конструкторы копирования, и конструктор шаблона может использоваться для копирования объекта, если он обеспечивает лучшее соответствие, чем другие конструкторы.

Это обычно называется нарезкой объектов и является достаточно известной проблемой, чтобы иметь отдельную статью в Википедии (хотя это лишь краткое описание проблемы).

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

Не совсем решение вашей непосредственной проблемы, но ....

Большинство функций, которые принимают объекты класса / структуры в качестве параметров, должны объявлять параметры типа «const X &» или «X &», если у них нет очень веской причины не делать этого.

Если вы всегда будете это делать, нарезка объекта никогда не будет проблемой (ссылки не будут нарезаны!).

Я бы предложил добавить к вашему базовому классу конструктор, который явно принимает константную ссылку на производный класс (с предварительным объявлением). В моем простом тестовом приложении этот конструктор вызывается в случае нарезки. Тогда вы могли бы, по крайней мере, получить утверждение во время выполнения, и вы, вероятно, могли бы получить утверждение во время компиляции с умным использованием шаблонов (например: создать экземпляр шаблона таким образом, чтобы генерировать утверждение во время компиляции в этом конструкторе). Также могут быть специфичные для компилятора способы получения предупреждений или ошибок во время компиляции при вызове явных функций; например, вы можете использовать «__declspec (устаревший)» для «конструктора среза» в Visual Studio, чтобы получить предупреждение во время компиляции, по крайней мере, в случае вызова функции.

Итак, в вашем примере код будет выглядеть так (для Visual Studio):

class Base { ...
    __declspec(deprecated) Base( const Derived& oOther )
    {
        // Static assert here if possible...
    }
...

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

Надеюсь это поможет. :)

Лучший способ справиться с этой проблемой - это обычно следовать рекомендациям Скотта Мейера (см. Эффективный C++ ) о наличии конкретных классов только в конечных узлах вашего дерева наследования и обеспечении того, чтобы нелистовые классы были абстрактными, имея по крайней мере одну чистую виртуальную функцию ( деструктор, если не что иное).

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

Редактировать

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

Мои рассуждения таковы.

Рассмотрим некоторый библиотечный код, который определяет класс Concrete1 и использует его в интерфейсе к этой функции.

void do_something( const Concrete1& c );

Передача типа be reference используется для повышения эффективности и в целом является хорошей идеей. Если библиотека считает Concrete1 типом значения, реализация может решить сделать копию входного параметра.

void do_something( const Concrete1& c )
{
    // ...
    some_storage.push_back( c );
    // ...
}

Если тип объекта переданной ссылки действительно является, Concrete1 а не каким-либо другим производным типом, тогда этот код в порядке, нарезка не выполняется. Общее предупреждение при push_back вызове этой функции может давать только ложные срабатывания и, скорее всего, будет бесполезным.

Рассмотрим некоторый код клиента , который унаследован Concrete2 от Concrete1 и передает его в другую функцию.

void do_something_else( const Concrete1& c );

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

Так где же ошибка? Что ж, «ошибка» заключается в передаче ссылки на что-то, производное от класса, который затем обрабатывается вызываемой функцией как тип значения.

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

Я немного изменил ваш код:

class Base{
  public:
    Base() {}
    explicit Base(const Base &) {}
};

class Derived: public Base {};

void Func(Base)
{

}

//void Func(Derived)
//{
//
//}

//main
int main() {
  Func(Derived());
}

Ключевое слово explicit гарантирует, что конструктор не будет использоваться как оператор неявного преобразования - вы должны вызывать его явно, когда хотите его использовать.