4

我有一组看起来像这样的类型:

struct MyFlag
{
     SomeId source_id; // INVALID_ID by default
     SomeData data; // regular type

     friend bool operator==( const MyFlag& a, const MyFlag& b ) { return a.source_id == b.source_id; }
     friend bool operator<( const MyFlag& a, const MyFlag& b ) { return a.source_id < b.source_id; }
     friend bool operator!=( const MyFlag& a, const MyFlag& b ) { return !(a == b); }

     friend bool operator==( const SomeId& a, const MyFlag& b ) { return a == b.source_id; }
     friend bool operator<( const SomeId& a, const MyFlag& b ) { return a < b.source_id; }
};



MyFlag flag_a { id, data_A };
MyFlag flag_b { id, data_B };

assert( flag_a == flag_b );
assert( flag_a.data != flag_b.data );
assert( flag_a == id );
assert( flag_b == id ); 

MyFlag flag = flag_b;
assert( flag == flag_a );
assert( flag == id );
assert( flag.data != flag_a.data );

const MyFlag flag_x ={ id_x, data_A };
flag = flag_X;
assert( flag != flag_a );
assert( flag.data == flag_a.data );

也就是说,在比较中只考虑对象状态的特定部分:在此示例中,任何 MyFlag 对象都将使用它们的 id 与其他对象进行比较,而不是它们包含的其余数据。

我认为它符合 Sean Parent 给出的“值类型”的定义,但我也认为这是一种奇怪或不熟悉(但在我的情况下很有用)的模式。

所以我的问题是:这个......概念有概念名称吗?


这种类型有什么用?我在“黑板”事件系统中使用这种类型,它基本上是一种具有至少常规类型的任何值的集合。但是,即使已经找到(通过比较),这个黑板也会系统地覆盖推送(插入)其中的值。这样,我使用比较运算符作为标识符覆盖黑板上值的完整状态。

我不知道这是否是众所周知的模式或想法,或者从长远来看是否存在问题。到目前为止,它非常有用。它也感觉像是“太聪明”的东西,但我缺乏这种模式的经验来证实这一点。可能是我滥用比较运算符的使用,但感觉这些类型的语义在我的使用中是正确的。

如有必要,我可以提供一个详细的使用示例。

4

5 回答 5

2

我想你可能会在John Lakos的这篇论文中找到答案,特别是在背景部分。简而言之,Lakos 区分了构成对象值的着属性与非显着属性(我记得它们也被称为附带属性,但可能是错误的)而不是(例如向量的容量) )。

于 2014-01-08T15:12:38.427 回答
2

MyFlagis not EqualityComparable,因为==对于具有不同值的对象返回 true。§3.3 中的定义EqualityComparable包括axiom { a == b <=> eq(a, b); }.

非正式地,eq表示我们认为是对象的的相等性,而不管==该对象的类型是否存在。这与表示相等并不严格相同,因为 (a) 不同的表示可以被认为是相等的(例如,-0.0 == 0.0),并且 (b) 表示中可能存在无关紧要的状态(俗称“填充”)。

在 的情况下MyFlag,我发现几乎可以肯定在某些情况下dataa 的值会被认为是重要的MyFlag(在 OP 本身中出现了几次)。形式上,我可以将运算符定义cmpMyFlag

bool cmp(const MyFlag& a, const MyFlag& b) {
  return a == b && a.data == b.data;
}

这显然比对应的 . 提供了对相等性更强的解释operator ==

考虑一个实现std::copy

template <typename In, typename Out>
Out copy_(In first, In last, Out out, std::false_type) {
  while(first != last) {
    *out++ = *first++;
  }
}

template <typename In, typename Out>
Out copy_(In first, In last, Out out, std::true_type) {
  while(first != last) {
    *out = *first;
    *out.data = SomeData();
    ++first;
    ++out;
  }
}

template <typename In, typename Out>
Out copy(In first, In last, Out out) {
  copy_(first, last, out, std::is_same<
          Myflag,
          typename std::iterator_traits<In>::value_type>());
}

您会认为这是 的有效实现copy,还是会说它正在破坏数据?根据Myflag' 是保持平等的operator ==

相比之下,Myflag被定义为:

class MyFlag
{
     SomeData trash_bits;
public:
     SomeId source_id; // INVALID_ID by default

     friend bool operator==( const MyFlag& a, const MyFlag& b ) { return a.source_id == b.source_id; }
     friend bool operator<( const MyFlag& a, const MyFlag& b ) { return a.source_id < b.source_id; }
     friend bool operator!=( const MyFlag& a, const MyFlag& b ) { return !(a == b); }

     friend bool operator==( const SomeId& a, const MyFlag& b ) { return a == b.source_id; }
     friend bool operator<( const SomeId& a, const MyFlag& b ) { return a < b.source_id; }
};

trash_bits您可以提出一个不属于 a 值的令人信服的论点,MyFlag因为它们从未被观察到。MyFlag我就同意了Regular

于 2014-01-08T10:30:27.937 回答
1

该类型具有定义总排序的正确比较运算符,因此是TotallyOrdered(使用N3351定义)。

这并没有区分总排序是否比较所有对象状态,但似乎没有任何区分它的概念。因为它既不可能定义(==根据状态的比较部分说对象相等,你怎么知道是否还有任何未比较部分?)也没有任何算法理由关心。

于 2014-01-08T08:53:45.557 回答
1

你所描述的似乎是一个非必要的部分。它与 std::vector 上的 capacity() 非常相似。正则的概念是根据复制、赋值和相等的语义定义的。只要遵守这些语义,您的类型就是常规的。您需要通过确定类型所代表的内容来确定类型的基本部分是什么。那些对对象所代表的东西有贡献的基本部分必须是副本并包含在相等比较中。

于 2015-06-08T05:19:52.337 回答
0

我认为您应该区分应用关系运算符的级别及其语义。您的运算符似乎具有正确的语义,但应用在一个令人困惑的级别(ID 成员,而不是整个对象)。

首先,我将定义operator==operator<比较整个对象 state。这是最不令人惊讶和最惯用的方式。要仅比较 id,只需创建一个命名运算符 id_equal_to,对 ID 数据成员进行投影。如果您愿意,您甚至可以定义混合版本(采用一个参数MyFlag和一个SomeID参数),但这通常只是为了避免隐式转换的开销。在这种情况下似乎不需要。

其次,要确保这些运算符具有正确的语义(对于 的自反、对称和传递,对于 是反operator==自反、不对称、传递和总),只需根据和对应的运算符来operator<定义它们。您还应该确保and on也具有正确的语义。对于内置函数,这是有保证的,但对于用户定义的 ID 类型,您可以再次应用相同的技巧。std::tiestd::tupleoperator==operator<SomeIdstd::tie

#include <cassert>
#include <tuple>

enum { invalid = -1 };
using SomeId = int;   // or any regular type with op== and op<
using SomeData = int; // or any regular type with op== and op<

struct MyFlag
{
    SomeId source_id; // INVALID_ID by default
    SomeData data;    // regular type

    friend bool operator==(MyFlag const& a, MyFlag const& b) 
    { return std::tie(a.source_id, a.data) == std::tie(b.source_id, b.data); }

    friend bool operator!=(MyFlag const& a, MyFlag const& b) 
    { return !(a == b); }

    friend bool operator<(MyFlag const& a, MyFlag const& b) 
    { return std::tie(a.source_id, a.data) < std::tie(b.source_id, b.data); }

    // similarly define >=, >, and <= in terms of !(a < b), (b < a) and !(b < a)

    friend bool id_equal_to(MyFlag const& a, MyFlag const& b)
    { return a.source_id == b.source_id; }    
};

int main()
{    
    auto const id = 0;
    auto const data_A = 1;
    auto const data_B = 2;

    MyFlag flag_a { id, data_A };
    MyFlag flag_b { id, data_B };

    assert( flag_a != flag_b );
    assert( id_equal_to(flag_a, flag_b) );
    assert( flag_a.data != flag_b.data );

    MyFlag flag = flag_b;
    assert( flag != flag_a );
    assert( id_equal_to(flag, flag_a) );
    assert( flag.data != flag_a.data );

    auto const id_x = invalid;
    const MyFlag flag_x = { id_x, data_A };
    flag = flag_x;
    assert( flag != flag_a );
    assert( id_equal_to(flag, flag_x) );
    assert( !id_equal_to(flag, flag_a) );
    assert( flag.data == flag_a.data );    
}

活生生的例子

于 2014-01-08T12:42:43.117 回答