如何使这个"template / constexpr"构造更优雅/不那么冗长?

How to make this "template / constexpr" construct more elegant / less verbose?

本文关键字:何使这 template constexpr      更新时间:2023-10-16

我有这个伪位域实现:

class Field {
public:
  constexpr Field(int i, int s) : index(i), size(s) {}
  constexpr Field(const Field & prev, int s) : index(prev.index + prev.size), size(s) {}
  int index, size;
};
#define FIELD(name, i, s) constexpr static const Field name = {i, s};
template<typename T = quint32>
class Flags {
public:
  Flags(T d = 0) : data(d) {}
  inline T readField(const Field & f) {
    return (data & getMask(f.index, f.size)) >> f.index;
  }
  inline void writeField(const Field & f, T val) {
    data = (data & setMask(f.index, f.size)) | (val << f.index);
  }
private:
  static constexpr T getMask(int i, int size) {
    return ((1 << size) - 1) << i;
  }
  static constexpr T setMask(int pos, int size) {
    return ~getMask(pos, size);
  }
  T data;
};

但是,以目前的形式使用它是相当冗长的:

struct Test {
  Flags<> flags;
  FIELD(one, 0, 1)
  FIELD(two, one, 2)
};
Test t;
t.flags.readField(t.one);
t.flags.writeField(t.one, 1);

我想让它更优雅,所以我可以简单地这样做,而不是上面的语法:

t.one.read();
t.one.write(1);

我尝试这样做的方法是为每个Field设置一个Flags &,并实现read()write()方法,这些方法在内部使用它的目标Flags

但是,这要求Field也成为模板,这进一步增加了详细程度,现在还必须为字段指定T

我尝试使用Flags<T>::makeField()隐式指定T但很快就变成了constexprtstatic和常规成员和方法、auto之类的不兼容的混乱,所以在兜圈子之后,最终决定向更有经验的人寻求建议。

当然,要求Fields不占用运行时存储,并在编译过程中解析尽可能多的表达式。

完全不知道你的意图是什么,我的第一个建议是简单地使用位域。 它简单/更快/等一千倍。

struct Test {
     unsigned long long one : 1;
     unsigned long long one : 2;
};

但是,如果你真的想要一个类,我做了一个FieldReference类,它似乎与你正在做的事情模糊匹配。

类:

#include <cassert>
#include <type_traits>
#include <cstddef>
template<class T, size_t offset_, size_t size_>
struct FieldReference {
    static const size_t offset = offset_;
    static const size_t size = size_;
    static const size_t mask = ~T(((~0)<<offset<<size)|((1<<offset)-1));
    explicit FieldReference(T& f) :flags(&f) {}
    operator T() const {return (flags[0]&mask)>>offset;}
    FieldReference& operator=(T v) {
        assert((v&~(mask>>offset))==0);
        flags[0] &= ~mask;
        flags[0] |= (v<<offset);
        return *this;
    }
private:
    T* flags;
};
#define FIRSTFIELD(Flags,Name,Size) 
    auto Name() -> FieldReference<decltype(Flags),0,Size> {return FieldReference<decltype(Flags),0,Size>(Flags);} 
    FieldReference<std::add_const<decltype(Flags)>::type,0,Size> Name() const {return FieldReference<std::add_const<decltype(Flags)>::type,0,Size>(Flags);}
#define FIELD(Flags,Name,Previous,Size) 
    auto Name() -> FieldReference<decltype(Flags),decltype(Previous())::offset,Size> {return FieldReference<decltype(Flags),decltype(Previous())::offset,Size>(Flags);} 
    auto Name() const -> FieldReference<std::add_const<decltype(Flags)>::type,decltype(this->Previous())::offset,Size> {return FieldReference<std::add_const<decltype(Flags)>::type,decltype(Previous())::offset,Size>(Flags);}

用法:

struct Test {
    unsigned long long flags = 0;
    FIRSTFIELD(flags,one,1);
    FIELD(flags,two,one,2);
};
#include <iostream>
int main() {
    Test t;
    t.one() = 1; //That seems less verbose
    t.two() = 3;
    std::cout << t.two();
    return 0;
}

http://coliru.stacked-crooked.com/a/c027d9829ce05119

字段不占用任何空间,除非在你处理它们的时候,即使这样,它们也只占用单个指针的空间。 所有偏移量、大小和掩码都是在编译时计算的,因此这也应该比您的代码更快。

以下是真正

接近您想要的内容的方法。请记住,您说过实现可能很丑陋。:)

template<class T, T mask, T bitpos>
class Field {
  T &d_t;
public:
  Field(T &t) : d_t(t) {}
  T read() const {
    return (d_t & mask) >> bitpos;
  }
  void write(T const &t)  {
    d_t = (d_t & ~mask) | (t << bitpos);
  }
};

#define BTFDENUMDECL1(name, width) name##start
#define BTFDENUMDECL2(name, width, ...) name##start, name##end =  name##start + width - 1, BTFDENUMDECL1(__VA_ARGS__)
#define BTFDENUMDECL3(name, width, ...) name##start, name##end = name##start + width - 1, BTFDENUMDECL2(__VA_ARGS__)
#define BTFDENUMDECL4(name, width, ...) name##start, name##end = name##start + width - 1, BTFDENUMDECL3( __VA_ARGS__)
#define BTFNMEMBER1(field, name, width) auto name() {           
    return Field<decltype(field), ((1 << width) - 1) << name##start, name##start>(field); }
#define BTFNMEMBER2(field, name, width, ...) BTFNMEMBER1(field, name, width) BTFNMEMBER1(field, __VA_ARGS__)
#define BTFNMEMBER3(field, name, width, ...) BTFNMEMBER1(field, name, width) BTFNMEMBER2(field, __VA_ARGS__)
#define BTFNMEMBER4(field, name, width, ...) BTFNMEMBER1(field, name, width) BTFNMEMBER3(field, __VA_ARGS__)
#define GET_MACRO(_1,_1_,_2,_2_,_3,_3_,_4,_4_, NAME, ...) NAME
#define BITFIELDS(field, ...)                       
  private: uint32_t field;                      
  enum E##flags { GET_MACRO(__VA_ARGS__, BTFDENUMDECL4,0, BTFDENUMDECL3,0, BTFDENUMDECL2,0, BTFDENUMDECL1,0)(__VA_ARGS__) }; 
public:                                 
 GET_MACRO(__VA_ARGS__, BTFNMEMBER4,0, BTFNMEMBER3,0, BTFNMEMBER2,0, BTFNMEMBER1,0)(field, __VA_ARGS__)

这是针对C++14,但可以"削减"到C++11。它最多支持 4 个字段,但您可以通过简单的克隆BTFDENUMDECLBTFNMEMBER宏以及更新GET_MACRO来添加更多字段。你会像这样使用它:

struct Test {
  BITFIELDS(flags,
    one, 1,
    two, 2,
    three, 3
    );
};

并在代码中:

Test test;
test.one().write(0);
std::cout << test.one().read() << std::endl;

因此,只有括号添加到所需的语法中。

快速解释:我们正在(ob)使用可变数量的宏参数。我们一次取两个参数(来自变量参数"list")- 你会发现大多数例子一次只取一个参数,所以这会有点不寻常。在BITFIELDS宏中,我们首先定义一个包含字段位位置的枚举,然后为每个字段定义一个函数。字段的函数返回具有 read()write() 方法的代理,该代理使用枚举中定义的位位置。

这是基本实现。您可以添加自己的花里胡哨,例如检查write()中的范围),另一个用于为字段提供类型的宏(如BITFIELDST(T, field, ...))等。

对于 C++98,您必须对其进行一些重构(以删除 __VA_ARGS__ 的使用),并通过在宏"调用"的名称中给出参数的数量来使用它,例如 BITFIELDS4 .

看起来不像其他人在咬人,所以我只提到想到的两种方法。

我认为这里的关键是一个字段,它能够在不占用任何存储空间的情况下修改特定值。因此,实现这一目标的突出语言功能将是:

匿名联合,提供test.one.read()类型的语法。

空基础优化,它将提供test.one().read()类型的语法。

前者的快速示例没有实际的位逻辑 - 此示例中的所有字段只是修改整个值。按位逻辑是微不足道的:

template< typename T >
struct Bitmask
{
    T m_val;
};
template< typename BITMASK, int INDEX >
struct Field : private BITMASK
{
    int read() const { return BITMASK::m_val; }
    void write( int i ) { BITMASK::m_val = i; }
};
struct Test
{
    typedef Bitmask<int> Flags;
    union
    {
        Flags m_flags;
        Field<Flags,0> one;
        Field<Flags,1> two;
        Field<Flags,2> three;
    };
};

这符合您的特定用法,但需要注意的是,该字段也是模板化的。作为旁注,我真的认为无论事情如何完成,它都应该是 test.m_flags.one.read() 或类似的,因为如果位是唯一但通用命名的,那么这允许任何具有 Flags 实例的类拥有多个它们没有问题。

带有函数的空基优化我没有模拟,但该函数将返回一个访问器对象 - 非常类似于示例中的字段,但所需的"Flags&"参数已经绑定。

空基地也可能需要单个继承和一些强制转换。从好的方面来说,我认为它可以精确地支持位掩码中的位数。因此,如果需要 3 位,它可能存储为无符号字符,但只存在函数 one()、two() 和 three()。

如果你愿意,如果我有时间,我也可以模拟这个例子。

据我所知,这两种技术都应该有效,所以我很想知道它们是否不可移植,如果是,出于什么原因。

编辑:快速阅读cppreference关于工会的部分表明,标准不支持从工会的非活跃成员读取。但是,主要编译器支持它。所以这种方法有一个问题。

首先,如果你想达到这样的效果:

int main() {
    Test t;
    cout << t.one.read() << endl;
    t.one.write(1);
    cout << t.one.read() << endl;
}

您必须通知onetwo他们将操纵flags - 因此第一个更改 - 添加了flags作为参数FIELD

struct Test {
  Flags<> flags;
  FIELD(one,          0, 1, flags);
//                          ^^^^^                                      
  FIELD(two, AFTER(one), 2, flags);
//           ^^^^^^^^^^
};

其他更改是我修改了将上一个标志应用于下一个标志index的方式 - 请参阅AFTER宏的使用。我相信它现在更具可读性,它简化了我的建议。 AFTER之后介绍,首先让我们讨论更重要的魔法。

因此,我引入了 FieldManip 类来操作给定的标志:

template <typename Flags, int i, int s>
class FieldManip {
public:
    constexpr static const Field field = {i, s};
    FieldManip(Flags* flags) : flags(flags) {}
    auto read()
    {
        return flags->readField(field);
    }
    template <typename T>
    void write(T value)
    {
        flags->writeField(field, value);
    }
private:
    Flags* flags;
};

它消耗了大小的额外内存:sizeof(Flags*),但好的编译器可以优化任何CPU开销。恐怕除了支付这个额外的内存之外别无他法,除非你想放宽你的要求 - 所以onetwo可能是成员函数,而不是成员变量......

因此,新FLAGS的定义很简单:

#define FIELD(name, i, s, flags) 
  FieldManip<decltype(flags), i, s> name{&flags}

是时候解释AFTER了:

首先,对Field进行了一些简化,删除了额外的构造函数并添加了独立函数after

class Field {
public:
  constexpr Field(int i, int s) : index(i), size(s) {}
  int index, size;
};
constexpr int after(const Field& prev) { return prev.index + prev.size; }

但是在宏中,我们FieldManip没有Field - 所以需要下一个函数:

template <typename FieldManip>
constexpr int after() { return after(FieldManip::field); }

AFTER

#define AFTER(fieldmanip) after<decltype(fieldmanip)>()
<</div> div class="answers">

为什么要调用方法?

首先,一个略有不同的标志:

template<class C, size_t N=0, class D=uint32_t>
struct Flags {
  Flags(Flags const&)=default;
  Flags():data() {} // remember to zero!
  Flags(D raw):data(raw) {}
  template<unsigned start, unsigned width>
  constexpr void set( D bits ) {
    D m = mask<start, width>();
    data &= ~m;
    data |= (bits<<start)&m;
  }
  template<unsigned start, unsigned width>
  constexpr D get( D bits ) const {
    D m = mask<start, width>();
    return (data&m)>>start;
  }
private:
  template<unsigned start, unsigned width>
  static constexpr D mask() {
    return ((1<<(width)-1)<<start;
  }
  D data;
};

我们的Flags现在输入了它所在的容器类型。

如果要在容器中包含多组Flags,请传递 n 的索引。

namespace details {
  template<class T> struct tag{using type=T;};
  template<class C, size_t n, unsigned start, unsigned width>
  struct field {};
  template<class Flags, class Field>
  struct pseudo_ref {
    Flags& flags;
    template<class U>
    constexpr pseudo_ref operator=( U&& u )const{
      field_assign( flags, Field{}, std::forward<U>(u) );
      return *this;
    }
    template<class T>
    constexpr operator T()const{
      return field_get( tag<T>{}, flags, Field{} );
    }
  };
  template<class C, size_t n, class D, unsigned start, unsigned width>
  constexpr auto operator*(
    Flags<C,n,D>& flags, field<C, n, start, width>
  )
  -> psuedo_ref<Flags<C,n,D>, field<C,n,start,width>>
  {
    return {flags};
  }
  template<class C, size_t n, class D, unsigned start, unsigned width>
  constexpr auto operator*(
    Flags<C,n,D> const& flags, field<C,n,start,width>
  )
  -> psuedo_ref<Flags<C,n,D> const, field<C,n,start,width>>
  {
    return {flags};
  }
  template<class C, size_t n, class D, unsigned start, unsigned width, class U>
  void field_assign( Flags<C,n,D>& flags, field<C,n,start,width>, U&& u ){
    flags.set<start, width>(std::forward<U>(u));
  }
  template<class T,class C, size_t n, class D, unsigned start, unsigned width>
  constexpr T field_get(
    tag<T>, Flags<C,n,D> const& flags, field<C,n,start,width>
  ) {
    return flags.get<start,width>();
  }
  template<class F>
  struct field_end;
  template<class C, size_t n, unsigned start, unsigned width>
  struct field_end:std::integral_constant<unsigned, start+width>{};
}
template<class C, unsigned width, size_t n=0>
using first_field = field<C,n,0,width>;
template<class C, class F, unsigned width, size_t n=0>
using next_field = field<C,n,details::field_end<F>::value,width>;

现在语法如下所示:

struct Test {
  Flags<Test> flags;
};
first_field<Test, 1> one;
next_field<Test, decltype(one), 2> two;
Test t;
uint32_t o = t*one;
t*one = 1;

并且所有内容都自动调度到位摆弄代码。

请注意完全缺少宏。 您可以使用一个来删除上面的decltype

我知道过于复杂和令人难以置信的 C++11 和 C++14 功能是时髦的,但这里有一个使用邪恶宏的快速而肮脏的解决方案:

#define GETMASK(index, size) (((1 << size) - 1) << index)
#define READFROM(data, index, size) ((data & GETMASK(index, size)) >> index)
#define WRITETO(data, index, size, value) (data = (data & (~GETMASK(index, size))) | (value << index))
#define FIELD(data, name, index, size) 
  inline decltype(data) name() { return READFROM(data, index, size); } 
  inline void set_##name(decltype(data) value) { WRITETO(data, index, size, value); }

然后简单地:

struct Test {
  uint flags;
  FIELD(flags, one, 0, 1)
  FIELD(flags, two, 1, 2)
};
t.set_two(3);
cout << t.two();

为字段生成访问器,就好像它们是包含对象的属性一样,它不那么冗长,并且 IMO 也非常可读,因为这是人们从封装数据中公开对象属性的常见方式。

缺点 - 您必须自己计算字段索引,但您仍然可以使用生成访问器的方法以及依赖于带有构造函数的静态字段的现有实现来避免它。

优点 - 它简短,简单,高效且向后兼容 - 将decltype更改为typeof,它将适用于pre-c++ 11甚至普通C。