Переменная-член класса C++, знающая свое смещение

Можно ли иметь переменную-член, которая сможет рассчитывать указатель на содержащий объект из указателя на себя (в его методе)?

Давайте создадим интерфейс внешнего вызова в API следующим образом:

template <typename Class, MethodId Id, typename Signature>
class MethodProxy;

template <typename Class, MethodId Id, typename ReturnT, typename Arg1T>
class MethodProxy<Class, Id, ReturnT ()(Arg1T) {
  public:
    ReturnT operator()(Class &invocant, Arg1T arg1);
};

и аналогично для других чисел аргументов от 0 до N. Для каждого класса на внешней стороне один класс C++ объявляется с некоторыми чертами, и этот шаблон использует эти черты (и больше черт для типов аргументов), чтобы найти и вызвать чужой метод. Это может быть использовано как:

Foo foo;
MethodProxy<Foo, barId, void ()(int)> bar;
bar(foo, 5);

Теперь то, что я хотел бы сделать, это определить Foo таким образом, что я могу назвать как:

Foo foo;
foo.bar(5);

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

template <typename Class, MethodId Id, typename Signature>
class MethodMember;
template <typename Class, MethodId Id, typename ReturnT, typename Arg1T>
class MethodMember<Class, Id, ReturnT ()(Arg1T) {
    MethodProxy<Class, Id, Signature> method;
    Class &owner;
  public:
    MethodMember(Class &owner) : owner(owner) {}
    ReturnT operator()(Arg1T arg1) { return method(owner, arg1); }
};

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

Я думал по линии

template <typename Class, size_t Offset, ...>
class Member {
    Class *owner() {
        return reinterpret_cast<Class *>(
            reinterpret_cast<char *>(this) - Offset);
    }
    ...
};
class Foo {
    Member<Foo, offsetof(Foo, member), ...> member;
    ...
};

но это жалуется, что Foo является неполным типом в данный момент.

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

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

Я знаю, как делать методы-оболочки, упомянутые выше, с boost.preprocessor, но списки аргументов должны быть указаны в странной форме. Я знаю, как написать макрос для генерации универсальных оболочек через шаблоны, но это, вероятно, дало бы плохую диагностику. Было бы также тривиально, если бы звонки могли выглядеть foo.bar()(5), Но я хотел бы знать, возможен ли какой-нибудь умный трюк (плюс, вероятно, только такой умный трюк, вероятно, будет использоваться для свойств).

Примечание. Тип элемента не может быть фактически специализирован ни на указателе элемента, ни на его смещении, поскольку тип должен быть известен до того, как может быть назначено это смещение. Это потому, что тип может влиять на требуемое выравнивание (рассмотрим явную / частичную специализацию).

4 ответа

Решение

Задать вопрос - это лучший способ понять ответ, так что вот где я получил:

Смещение не может быть аргументом шаблона, потому что тип должен быть известен до того, как смещение может быть вычислено. Поэтому он должен быть возвращен функцией аргумента. Давайте добавим тип тега (фиктивная структура) и либо поместим перегруженную функцию в Owner, либо непосредственно в тег. Таким образом, мы можем определить все, что нам нужно, в одном месте (используя макрос). Следующий код прекрасно компилируется с gcc 4.4.5 и выводит правильный указатель для всех членов:

#include <cstddef>
#include <iostream>

using namespace std;

(просто преамбула, чтобы она действительно скомпилировалась)

template <typename Owner, typename Tag>
struct offset_aware
{
    Owner *owner()
    {
        return reinterpret_cast<Owner *>(
            reinterpret_cast<char *>(this) - Tag::offset());
    }
};

Это то, что нужно, чтобы объект осознал свое смещение. Свойство, функтор или какой-либо другой код можно свободно добавлять, чтобы сделать его полезным. Теперь нам нужно объявить некоторые дополнительные вещи вместе с самим членом, поэтому давайте определим этот макрос:

#define OFFSET_AWARE(Owner, name) \
    struct name ## _tag { \
        static ptrdiff_t offset() { \
            return offsetof(Owner, name); \
        } \
    }; \
    offset_aware<Owner, name ## _tag> name

Это определяет структуру как тег и добавляет функцию, возвращающую требуемое смещение. Чем он определяет сам элемент данных.

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

struct foo
{
    int x;
    OFFSET_AWARE(foo, a);
    OFFSET_AWARE(foo, b);
    OFFSET_AWARE(foo, c);
    int y;
};

Просто, не правда ли?

int main()
{
    foo f;

    cout << "foo f = " << &f << endl
        << "f.a: owner = " << f.a.owner() << endl
        << "f.b: owner = " << f.b.owner() << endl
        << "f.c: owner = " << f.c.owner() << endl;
    return 0;
}

Это печатает одно и то же значение указателя на всех строках. Стандарт C++ не позволяет членам иметь размер 0, но они будут иметь размер своего фактического содержимого или 1 байт, если они в противном случае пустые по сравнению с 4 или 8 (в зависимости от платформы) байтами для указателя.

1) Есть расширение gcc, которое кажется подходящим:

enum{ d_y = __builtin_choose_expr(N,offsetof(X,y),0) };

Но это не сработало, как ожидалось, хотя руководство говорит
"встроенная функция не оценивает выражение, которое не было выбрано"

2) указатели членов показались интересными, например. offsetof может быть определен так:

template< class C, class T >
int f( T C::*q ) {
  return (int)&((*(C*)0).*q);
}

Но я все еще не нашел способ превратить это в constexpr.

3) А пока вот еще одна версия:

#include <stdio.h>

#pragma pack(1)

template <class A, int x>
struct B {
  int z;
  void f( void ) {
    printf( "x=%i\n", x );
  }
};

#define STRUCT( A ) template< int N=0 > struct A {
#define CHILD( A, N, B, y ) }; template<> struct A<N> : A<N-1> \
  { B<A<N>,sizeof(A<N-1>)> y;
#define STREND };

STRUCT( A )
  int x0;
  int x1;
  CHILD( A,1, B, y );
  short x2;
  CHILD( A,2, B, z );
  char x3;
STREND

typedef A<2> A1;

int main( void ) {
  A1 a;
  a.y.f();
  a.z.f();
}

Пока вот одно решение для MS, все еще думающее, как сделать его более общим

#include <stdio.h>

#define offs(s,m)   (size_t)&(((s *)0)->m)
#define Child(A,B,y) \
  __if_exists(X::y) { enum{ d_##y=offs(X,y) }; } \
  __if_not_exists(X::y) { enum{ d_##y=0 }; } \
  B<A,d_##y> y;

template <class A, int x>
struct B {
  int z;
  void f( void ) {
    printf( "x=%i\n", x );
  }
};

template< class X >
struct A {
  int x0;
  int x1;
  Child(A,B,y);
  Child(A,B,z);
};

typedef A<int> A0;

typedef A<A0> A1;

int main( void ) {
  A1 a;
  a.y.f();
  a.z.f();
}

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

Другие вопросы по тегам