Как проверить правильность reinterpret_cast во время компиляции

#c

#c

Вопрос:

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

Чтобы быть конкретным, сценарий следующий: у меня есть Base класс и Derived класс (очевидно, производный от Base ). Существует также шаблонный Wrapper класс, который состоит из указателя на класс, указанный в качестве параметра шаблона.

 class Base
{
    // ...
};

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

template <class T>
class Wrapper
{
    T* m_pObj;
    // ...
};
  

В некоторых ситуациях у меня есть переменная типа Wrapper<Derived> , и я хотел бы вызвать функцию, которая получает (const) ссылку ro Wrapper<Base> . Очевидно, что здесь нет автоматического приведения, Wrapper<Derived> не производного от Wrapper<Base> .

 void SomeFunc(const Wrapper<Base>amp;);

Wrapper<Derived> myWrapper;
// ...

SomeFunc(myWrapper); // compilation error here
  

Существуют способы справиться с этой ситуацией в рамках стандарта C . Например, так:

 Derived* pDerived = myWrapper.Detach();

Wrapper<Base> myBaseWrapper;
myBaseWrapper.Attach(pDerived);

SomeFunc(myBaseWrapper);

myBaseWrapper.Detach();
myWrapper.Attach(pDerived);
  

Но мне это не нравится. Это не только требует неудобного синтаксиса, но и создает дополнительный код, поскольку Wrapper имеет нетривиальный d’tor (как вы уже догадались), и я использую обработку исключений. OTOH, если указатель на Base и Derived совпадает (как в этом примере, поскольку множественного наследования нет) — можно просто привести myWrapper к нужному типу и вызвать SomeFunc , и это сработает!

Поэтому я добавил следующее к Wrapper :

 template <class T>
class Wrapper
{
    T* m_pObj;
    // ...

    typedef T WrappedType;


    template <class TT>
    TTamp; DownCast()
    {
        const TT::WrappedType* p = m_pObj; // Ensures GuardType indeed inherits from TT::WrappedType

        // The following will crash/fail if the cast between the types is not equivalent to reinterpret_cast
        ASSERT(PBYTE((WrappedType*)(1)) == PBYTE((TT::WrappedType*)(WrappedType*)(1)));

        return (TTamp;) *this; // brute-force case
    }

    template <class TT> operator const Wrapper<TT>amp; () const
    {
        return DownCast<Wrapper<TT> >();
    }
};


Wrapper<Derived> myWrapper;
// ...

// Now the following compiles and works:
SomeFunc(myWrapper);
  

Проблема в том, что в некоторых случаях приведение методом перебора недопустимо. Например, в этом случае:

 class Base
{
    // ...
};

class Derived
    :public AnotherBase
    ,public Base
{
    // ...
};
  

Здесь значение указателя на Base отличается от Derived . Следовательно Wrapper<Derived> , не эквивалентно Wrapper<Base> .

Я хотел бы обнаруживать и предотвращать попытки такого недопустимого понижения. Я добавил проверку (как вы можете видеть), но она работает во время выполнения. То есть код будет скомпилирован и запущен, и во время выполнения произойдет сбой (или сбой утверждения) в отладочной сборке.

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

Есть ли способ добиться этого?

Комментарии:

1. Возможно, использование a static_cast here return (TTamp;) *this; // brute-force case вместо c-cast может помочь, c-cast может выполнять reinterpret_cast, что действительно все сломает. Если оба класса разделяют наследование, компилятор должен заставить указатели указывать на нужные места static_cast .

2. @RedX: в этом конкретном месте это эквивалентно static_cast , поскольку классы Wrapper<Base> rwo и Wrapped<Derived> не связаны

Ответ №1:

Короткий ответ: нет.

Длинный ответ:

Во время компиляции доступен ограниченный самоанализ, и вы можете, например (используя разрешение перегрузки функций), определить, является ли класс B доступным базовым классом другого класса D.

Однако это все.

Стандарт не требует полного самоанализа и, в частности:

  • вы не можете перечислить (прямые) базовые классы класса
  • вы не можете знать, имеет ли класс только один или несколько базовых классов
  • вы даже не можете знать, является ли базовый класс первым базовым или нет

И, конечно, существует проблема, связанная с тем, что расположение объекта в любом случае более или менее не определено (хотя C 11 добавляет возможность различать тривиальный макет и класс с помощью виртуальных методов, если я правильно помню, что здесь немного помогает!)

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

Поэтому, несмотря на ваше смелое утверждение P.S. Пожалуйста, не отвечайте «почему вы хотите это сделать» или «это противоречит стандарту». Я знаю, для чего все это, и у меня есть свои причины для этого., вам придется адаптировать свои способы.

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


Как реализовать подобную систему?

Я бы предложил, во-первых, простое решение:

  • Wrapper<T> является ли класс owner не копируемым, неконвертируемым
  • WrapperRef<U> реализует прокси поверх существующего Wrapper<T> (до тех пор, пока T* он конвертируется в U* ) и предоставляет средства преобразования.

Мы будем использовать тот факт, что все указатели, с которыми нужно манипулировать, наследуются UnkDisposable (это важная информация!)

Код:

 namespace details {
  struct WrapperDeleter {
    void operator()(UnkDisposable* u) { if (u) { u->Release(); } }
  };


  typedef std::unique_ptr<UnkDisposable, WrapperDeleter> WrapperImpl;
}

template <typename T>
class Wrapper {
public:
  Wrapper(): _data() {}

  Wrapper(T* t): _data(t) {}

  Wrapper(Wrapperamp;amp; right): _data() {
    using std::swap;
    swap(_data, right._data);
  }

  Wrapperamp; operator=(Wrapperamp;amp; right) {
    using std::swap;
    swap(_data, right._data);
    return *this;
  }

  T* Get() const { return static_cast<T*>(_data.get()); }

  void Attach(T* t) { _data.reset(t); }
  void Detach() { _data.release(); }

private:
  WrapperImpl _data;
}; // class Wrapper<T>
  

Теперь, когда мы заложили основы, мы можем создать наш адаптивный прокси. Поскольку мы будем манипулировать всем только до конца WrapperImpl , мы обеспечиваем безопасность типов (и осмысленность наших static_cast<T*> ), проверяя преобразования через std::enable_if и std::is_base_of в конструкторах шаблонов:

 template <typename T>
class WrapperRef {
public:
  template <typename U>
  WrapperRef(Wrapper<U>amp; w,
    std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
    _ref(w._data) {}

  // Regular
  WrapperRef(WrapperRefamp;amp; right): _ref(right._ref) {}
  WrapperRef(WrapperRef constamp; right): _ref(right._ref) {}

  WrapperRefamp; operator=(WrapperRef right) {
    using std::swap;
    swap(_ref, right._ref);
    return *this;
  }

  // template
  template <typename U>
  WrapperRef(WrapperRef<U>amp;amp; right,
    std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
    _ref(right._ref) {}

  template <typename U>
  WrapperRef(WrapperRef<U> constamp; right,
    std::enable_if_c< std::is_base_of<T, U> >::value* = 0):
    _ref(right._ref) {}

  T* Get() const { return static_cast<T*>(_ref.get()); }

  void Detach() { _ref.release(); }

private:
  WrapperImplamp; _ref;
}; // class WrapperRef<T>
  

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

С другой стороны, вы также можете обогатить это, используя shared_ptr / weak_ptr подход, чтобы иметь возможность копировать и перемещать оболочку и при этом гарантировать доступность (но остерегайтесь утечек памяти).

Примечание: это намеренно, что WrapperRef не предоставляет Attach метод, такой метод нельзя использовать с базовым классом. В противном случае, используя оба Apple и Banana производные от Fruit , вы могли бы присоединить a Banana через a WrapperRef<Fruit> , даже если оригинал Wrapper<T> был a Wrapper<Apple>

Примечание: это легко из-за общего UnkDisposable базового класса! Это то, что дает нам общий знаменатель ( WrapperImpl ) .

Комментарии:

1.Большое спасибо за ответ. Я подумал о проверке во время компиляции, своего рода выражении, которое может быть вычислено во время компиляции и подвергнуто STATIC_ASSERT макросу (надеюсь, вы знакомы с этой техникой). Кстати, выражение, которое я использовал, теоретически PBYTE((WrappedType*)(1)) == PBYTE((TT::WrappedType*)(WrappedType*)(1)) может быть вычислено во время компиляции. Я уверен, что при сборке релиза с включенной оптимизацией компилятор заменит известный результат. Однако компилятор, который я использую (msvc), не допускает этого.

2. Более широкая картина. У меня есть базовый интерфейс UnkDisposable , который имеет virtual Release() = 0; . Это оболочка Wrapper RAII, которая переносит указатель на такой объект и проверяет Release , вызывается ли он при необходимости. Wrapper это шаблонный класс, который обертывает любой класс, который наследуется от UnkDisposable . Классы реализуются UnkDisposable по-другому: некоторые немедленно удаляют объект, тогда как другие реализуют подсчет ссылок (у них также есть AddRef ). продолжение…

3. Существует функция, которая принимает некоторые параметры и отвечает за инициирование асинхронной операции. Если это удастся — он должен стать владельцем соответствующих выделенных ресурсов. Существует интерфейс CompletionHandler , унаследованный от UnkDisposable , который добавляет соответствующие методы обработки завершения / ошибок. Функция принимает Wrapper<CompletionHandler>amp; в качестве параметра. Если все в порядке — он отделяет выделенный объект от заданной оболочки и становится его владельцем (присоединяет его к своей внутренней оболочке). продолжение…

4. Есть класс MyCompletionHandler , производный от CompletionHandler . Он выделяется, инициализируется и передается функции. Я хочу иметь возможность сделать это: Wrapper<MyCompletionHandler> pHandler(new MyCompletionHandler); а затем использовать его в качестве аргумента, где Wrapper<CompletionHandler> ожидается или Wrapper<UnkDisposable>

5. @valdo: есть ли какая-либо причина, по которой вы не можете использовать более традиционный RAII на основе деструктора (в частности, интеллектуальные указатели), вместо того, чтобы пытаться эмулировать C #?