LINUX.ORG.RU

C++ каст при передаче по ссылке

 ,


0

4

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

Вроде все работает, но вдруг здесь undefined behavior.

#include <iostream>

template <typename T>
class Base {
public:
	int getX() const {
		return static_cast<const T*>(this)->x;
	}
};

class Derived : public Base<Derived> {
public:
	int x = 5;
};


template <typename T>
void foo(const Base<T>& a) {
	std::cout << a.getX() << std::endl;
}

int main() {
	
	Derived a;
	foo(a); // выводит 5, это UB?
	
	Base<Derived> b;
	foo(b); // ожидаемо выводит мусор
	
	return 0;
}
★★

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

Может, тут ничего криминального нет

return static_cast<const T*>(this)->x;

а вот здесь - есть

yoghurt ★★★★★ ()
Ответ на: комментарий от yoghurt

Может, тут ничего криминального нет

То есть там таки UB?

Я не пойму, может ли в определенный момент, на определенном компиляторе, вместо 5 появиться мусор?

а вот здесь - есть

Что?

Kuzy ★★ ()
Последнее исправление: Kuzy (всего исправлений: 1)
Ответ на: комментарий от Kuzy

То есть там таки UB?

Да нет уб, вызовется правильный виртуальный метод.

Что?

this == T * const, а не const T*, поэтому кастовать правильно в Derived * const. А вообще, я имел в виду вот это:

#include <cstdio>

struct Lol { 
   int a;
   int c;

   virtual ~Lol() {}
};

template<class Derived>
struct Foo {
   int d;
   int e;
   int f;
   
   virtual void test()
   {
      printf("this obj:   %x\n"
             "derived(1): %x\n"
             "derived(2): %x\n",
             this,
             static_cast<Derived * const>(this),
             dynamic_cast<Derived * const>(this));
   }
};

struct Bar: public Foo<Bar>, public Lol {};
struct Baz: public Lol, public Foo<Baz> {};

int main (int argc, char *argv[]) {
   Bar bar;
   Baz baz;

   printf("bar: %x\n", &bar);
   bar.test();

   printf("baz: %x\n", &baz);
   baz.test();

   printf("pointer test\n");
   Foo<Baz> *pFoo = &baz;
   printf("pFoo:       %x\n"
          "derived(1): %x\n"
          "derived(2): %x\n",
          pFoo,
          static_cast<Baz*>(pFoo),
          dynamic_cast<Baz*>(pFoo));
   return 0;
}

Смотрю на это дело и пытаюсь понять, почему я думал, что static_cast вернет тот же адрес, что сам this, для случая Baz. Видимо, сам что-то упустил.

/tmp $ ./a.out 
bar: 5603c5e0
this obj:   5603c5e0
derived(1): 5603c5e0
derived(2): 5603c5e0
baz: 5603c610
this obj:   5603c620
derived(1): 5603c610
derived(2): 5603c610
pointer test
pFoo:       5603c620
derived(1): 5603c610
derived(2): 5603c610
yoghurt ★★★★★ ()

5.2.9/11 Static cast

A prvalue of type “pointer to cv1 B,” where B is a class type, can be converted to a prvalue of type “pointer to cv2 D,” where D is a class derived (Clause 10) from B, if a valid standard conversion from “pointer to D” to “pointer to B” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is neither a virtual base class of D nor a base class of a virtual base class of D. The null pointer value (4.10) is converted to the null pointer value of the destination type. If the prvalue of type “pointer to cv1 B” points to a B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the result of the cast is undefined.

В таком случае — когда ты обязываешься передавать в foo() именно T — наследника Base<T> — можно и это не будет UB. Если ты налажал и передал не то — будет UB.

ilammy ★★★ ()
Ответ на: комментарий от ilammy

Если он налажет у него код не скомпилячится.

ossa ()
Ответ на: комментарий от ilammy

Гы, пытаясь предотвратить компиляцию во втором случае, наткнулся на разное поведение clang и gcc.

#include <iostream>

template <typename T>
class Base {
protected: // !!!
	Base() = default;
	Base(const Base&) = default;
public:
	int getX() const {
		return static_cast<T const*>(this)->x;
	}
};

class Derived : public Base<Derived> {
public:
	int x = 5;
};


template <typename T>
void foo(const Base<T>& a) {
	std::cout << a.getX() << std::endl;
}

int main() {
	Derived a;
	foo(a); 
	
	// clang не компилирует, говорит что конструктор protected, gcc так не думает
	Base<Derived> b;
	foo(b);
	
	// и clang, и gcc не компилируют
	Base<Derived> c = b;
	
	return 0;
}

Похоже на баг gcc.

Kuzy ★★ ()
Ответ на: комментарий от ossa

Не обязательно. Он может передать Base<Foo>, он может передать Bar : public Base<Foo>.

ilammy ★★★ ()
Ответ на: комментарий от ilammy

не понял. Как-то так?


struct Foo {
};

struct A: public Base<Foo> {
    int x;
};
...

Base<A> b;
foo(b);

не, не взлетит.

ossa ()
Ответ на: комментарий от ossa

Оу, да, ступил. Вот так нельзя. Но можно и такой вариант сделать:

struct Super : public Base<Derived> {
    int y = 10;
    int x = 15;
};

// . . .

Super c;
foo(c);
// выводит внезапно 10, или что там в Super по смещению Derived::x

ilammy ★★★ ()
Ответ на: комментарий от Kuzy

Если раскомментировать, то gcc перестает компилировать. Видимо он игнорирует конструкторы для POD.

#include <iostream>

class A {
protected:
	A() = default;
//	virtual void foo() {}
};

int main() {
	A g;
	
	return 0;
}

Kuzy ★★ ()
Ответ на: комментарий от ilammy

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

ossa ()
Ответ на: комментарий от ossa

Ну, можно выкрутиться:

#include <iostream>

//
// Исходные определения
//

template <typename T>
struct Base
{
    int getX() const
    {
        return static_cast<const T*>(this)->x;
    }
};

struct Derived : public Base<Derived>
{
    int x = 5;
};

struct Super : public Base<Derived>
{
    int y = 10;
    int x = 15;
};

//
// Compile-time проверка на наследование
//

template <typename Base, typename Derived>
struct inherits_from
{
    typedef char (&yes)[1];
    typedef char  (&no)[2];

    struct check
    {
        operator Base*() const;
        operator Derived*();
    };

    template <typename T>
    static yes really(Derived*, T);
    static no  really(Base*,  int);

    static const bool value = sizeof(really(check(), int())) == sizeof(yes);
};

//
// Compile-time проверка на идентичность типов
//

template <typename A, typename B>
struct equal_types
{
    static const bool value = false;
};

template <typename T>
struct equal_types<T, T>
{
    static const bool value = true;
};

//
// Собсно код
//

template <typename T>
void foo(const T& arg)
{
    static_assert(inherits_from<Base<T>, T>::value
            and not equal_types<Base<T>, T>::value,
        "Expected arg to be of type T inherited from Base<T>");

    std::cout << arg.getX() << std::endl;
}

int main()
{
    foo(Derived());         //  5

    foo(Base<Derived>());   //  ошибка компиляции

    foo(Super());           //  ошибка компиляции
}

ilammy ★★★ ()

Случайно зашёл в вашу ветку, повторяя плюсы. Поясните плс почему foo(b) выводить мусор?

LIKAN ★★★ ()
Ответ на: комментарий от LIKAN
// фактически в памяти последовательно расположенны члены Derived и Base<Derived>
// this внутри getX указывает на начало Base, 
// внутри getX, this кастуется к Derived*, полученный указатель указывает на начало Derived
Derived a;
a.getX();

// тоже самое, но в памяти только Base<Derived>, если кастануть this к Derived*, 
// то полученный указатель будет указывать на мусор рядом с Base<Derived>, что и происходит
Base<Derived> b;
b.getX();
Kuzy ★★ ()
Последнее исправление: Kuzy (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.