(重新)命名std::pair成员

36

我想使用town->name而不是town->first。内联命名访问器(重命名映射迭代器的第一和第二项以及 命名std::pair成员)是我目前找到的最佳解决方案。 我对命名访问器的问题在于类型安全丢失:pair<int,double>可能指代struct { int index; double value; }或者struct { int population; double avg_temp; }。 是否有人能提出一个简单的方法,也许类似于traits?

我经常想从函数返回一对或元组,每次都引入一个新类型(如struct city { string name; int zipcode; }及其构造函数)很烦人。 我很高兴了解到boost和C++0x,但我需要一个纯C++03解决方案,没有boost。

更新

回答andrewdski的问题:是的,语法类似于pair<int=index, double=value> (将创建与pair<int=population, double=avg_temp>不同的类型)将满足您的要求。我甚至不介意只需一次实现自定义pair/tuple模板类,然后在需要新类型时适当传递“名称traits”模板参数。 我不知道这个“名称traits”会是什么样子。也许这是不可能的。


5
为什么这不是一个真正的问题?为什么会被踩? - GManNickG
2
@Bo Persson:在你给我点踩之前,你应该先读一下问题:我在哪里提到了继承? - Ali
4
有 std::tuple / boost::tuple 用于更大的对象。 - Karl von Moor
2
那么,假设有这样一种语法:pair<int=index, double=value>,它将创建一个与pair<int=population, double=avg_temp>不同的类型,这符合您的要求吗? - andrewdski
2
@Bo:没有附言就进行负评是因为某人不理解某事或存在误解吗?那你一定对每个问题都进行了负评。 (请注意,他根本没有提到继承,所以我不太确定你的观点是什么。) - GManNickG
显示剩余14条评论
10个回答

27

我不明白你怎样能做得比这更好

struct city { string name; int zipcode; };

这里没有不必要的东西。你需要这两个成员的类型,你的整个问题都是围绕给这两个成员命名,并希望它成为唯一类型。

你知道聚合初始化语法吗?你不需要构造函数或析构函数,编译器提供的就可以了。

例如:http://ideone.com/IPCuw


类型安全要求引入新类型,否则pair<string,int>在(名称,邮政编码)和(人口,温度)之间会有歧义。

在C++03中,返回新元组需要以下两种方式之一:

city retval = { "name", zipcode };
return retval;

或者编写一个方便的构造函数:

city::city( std::string newName, int newZip ) : name(newName), zipcode(newZip) {}

获取

return city("name", zipcode);

然而,使用C++0x,你将被允许编写

return { "name", zipcode };

并且不需要定义用户自定义的构造函数。


3
你比我先说出来了。不过我需要补充一点,使用 string 时,聚合初始化语法是不起作用的。 - Alexandre C.
2
可能需要对聚合初始化语法进行扩展,以适应广大的谷歌用户。 - Doug T.
@Ben Voigt:点赞并感谢您。不幸的是,在C++03中没有扩展初始化列表,因此您不能在函数末尾编写return { "new york", 10001 };。因此,您仍然需要编写一个构造函数并编写return city("new york", 10001);。我需要一个纯C++03解决方案。 - Ali
@Ben Voigt:我想接受你的答案,但请添加以下内容。(1)您必须引入新类型,否则您无法同时拥有typedef pair<string /*name*/,int /*zipcode*/> city;typedef pair<string /*street*/,int /*number*/> building;(2)在C++0x中,您将能够编写return { "new york", 10001 };(3)与此同时,您可以编写一次构造函数,或者每次返回城市时添加额外的一行city retval = { "new york", 10001 }; return retval;。谢谢! - Ali
@bluefeet:不,很遗憾ideone的服务条款声明它是一个“永久”发布源代码片段的地方,但实际上他们销毁用户拥有的材料并且不回应询问。因此,我基本上已经停止使用它了。 - Ben Voigt
显示剩余6条评论

6

I guess elaborating on

struct City : public std::pair<string, int> {
  string& name() { return first; }
  const string& name() const { return first; }
  int& zip() { return second; }
  int zip() const { return second; }
};

虽然struct City { string name; int zipcode; }看起来完全没问题,但是is the closest you get to what youre looking for是你所需要的最接近的。


2
“内联命名访问器(12)是我目前发现的最佳解决方案。” - sehe

5
虽然不完美,但可以使用标记数据:
template <typename tag_type, typename pair_type>
typename tag_type::type& get(pair_type& p);

typedef std::pair<std::string /*name*/, int /*zipcode*/> city;
struct name { typedef std::string type; };
struct zipcode { typedef int type; };

template <>
std::string& get<name, city>(city& city)
{
   return city.first;
}

template <>
int& get<zipcode, city>(city& city)
{
   return city.second;
}

int main()
{
   city c("new york", 10001);
   std::string n = get<name>(c);
   int z = get<zipcode>(c);
}

但正如Ben Voigt所说:struct city { string name; int zipcode; };几乎总是更好的选择。

编辑:模板可能会过度,您可以使用命名空间中的自由函数。这仍然无法解决类型安全问题,因为任何std::pair<T1, T2>与任何其他std::pair<T1, T2>都是相同的类型:

namespace city
{
   typedef std::pair<std::string /*name*/, int /*zipcode*/> type;

   std::string& name(type& city)
   {
      return city.first;
   }

   int& zipcode(type& city)
   {
      return city.second;
   }
}

int main()
{
   city::type c("new york", 10001);
   std::string n = city::name(c);
   int z = city::zipcode(c);
}

1
不得不点赞这个... 这正是我建议使用的(boost::bimaps)所做的。 - sehe
感谢您努力回答我的问题。然而,我仍然不明白。为什么在结构体中要有那些typedefs?它们似乎没有被使用。类型安全问题也没有解决,您无法再使用typedef std::pair<std::string /*street*/, int /*number*/> building;。这正是我对命名访问器的疑惑所在。 - Ali
@Ali:所有的std::pair<T1, T2>都和其他的std::pair<T1, T2>是相同类型。如果你想要类型安全,你需要使用不同的类型。 - dalle
是的,我知道。即使要实现一次自定义配对/元组模板类并在需要新类型时适当地传递“名称特征”模板参数,我也不介意。我不知道那个“名称特征”会是什么样子。也许这是不可能的。你明白我想要什么吗? - Ali
我已经点赞并感谢你的回答。也许一个简单的 struct city { string name; int zipcode; }; 就是最简单的解决方案,而且在 C++0x 中我将能够使用聚合初始化。 - Ali

4

由于std::pair通常用于在std::map容器中存储条目,因此您可能希望查看Boost Bimap中的标记元素

简介:

#include <boost/bimap/bimap.hpp>
#include <string>
#include <iostream>

struct name {}; // Tag for the default 'first' member
struct zipcode {}; // Tag for the default 'second' member

int main()
{
    using namespace boost::bimaps;
    typedef bimap <tagged<std::string, name>, tagged<int, zipcode> > Cities;
    typedef Cities::value_type registration;

    Cities cities;
    cities.insert(registration("Amsterdam", 20));
    cities.insert(registration("Rotterdam", 10));

    // ...
    std::string cityName;
    std::cin >> cityName;

    Cities::map_by<name>::const_iterator id_iter = cities.by<name>().find(cityName);
    if( id_iter != cities.by<name>().end() )
    {
        std::cout << "name: " << id_iter->get<name>() << std::endl
                  << "zip: " << id_iter->get<zipcode>()   << std::endl;
    }

    return 0;
}

请注意,bimap可以透明地模拟std::map或其他关联容器类型,而不会影响性能;它们只是更加灵活。在这个特定的例子中,定义最好改成类似于以下内容:
typedef bimap <tagged<std::string, name>, multiset_of<tagged<int, zipcode> > > Cities;
typedef Cities::value_type registration;

Cities cities;
cities.insert(registration("Amsterdam", 20));
cities.insert(registration("Rotterdam", 10));
cities.insert(registration("Rotterdam", 11));

我邀请您浏览Boost Bimap文档,以便全面了解。


谢谢你的回答。我很高兴能了解更多关于boost的知识。请看一下我的评论,对sehe的回答,我有一些理解上的困难。 - Ali

1
您可以使用成员指针运算符。有几种替代方案。这是最直接的方法。
typedef std::map< zipcode_t, std::string > zipmap_t;
static zipcode_t const (zipmap_t::value_type::*const zipcode)
                                              = &zipmap_t::value_type::first;
static std::string (zipmap_t::value_type::*const zipname)
                                              = &zipmap_t::value_type::second;

// Usage
zipmap_t::value_type my_map_value;
std::string &name = my_map_value.*zipname;

您可以将一个伪类型的访问器放入专用的命名空间中,以将它们与其他内容分开。然后它看起来像my_map_value.*zip::name。但是,除非您真的需要使用pair,否则最好只需定义一个新的struct


是的,我认为如果一个人定义一个新的结构体,代码会更容易理解。如果必须使用对,我会使用内联访问器。无论如何,感谢您的回答,已点赞! - Ali

1
我想到了一个名为Utility_pair的宏,可以这样使用:
Utility_pair(ValidityDateRange,
    time_t, startDay,
    time_t, endDay
);

那么,每当您需要访问ValidityDateRange的字段时,可以像这样操作:

ValidityDateRange r = getTheRangeFromSomewhere();

auto start = r.startDay(); // get the start day
r.endDay() = aNewDay();    // set the end day
r.startDay(aNewDay1())     // set the start and end day in one go.
 .endDay(aNewDay2());

这是实现代码:
#include <utility>

#define Utility_pair_member_(pairName, ordinality, type, name)      \
    const type &name() const { return ordinality; }                 \
    type &name() { return ordinality; }                             \
    pairName &name(const type &m) { ordinality = m; return *this; } \
/***/

#define Utility_pair(pairName, firstMemberType, firstMemberName, secondMemberType, secondMemberName) \
    struct pairName: std::pair<firstMemberType, secondMemberType>  {                                 \
        Utility_pair_member_(pairName, first, firstMemberType, firstMemberName)                      \
        Utility_pair_member_(pairName, second, secondMemberType, secondMemberName)                   \
    }                                                                                                \
/***/

1
也许不值得为了更多的内存而这样做,但如果你想保留 std::pairstd::tuple 的优势 / 与不可更改的 API 兼容,你可以从它们继承然后定义对其成员的引用。
除了内存成本之外,这种方法有一个巨大的警告:STL 类型通常没有虚析构函数,因此如果你尝试使用基类型的指针进行释放,则可能会导致内存泄漏。
#include <tuple>

struct PairShadow : public std::pair<int, double> {
    using std::pair<int, double>::pair;
    PairShadow & operator=(PairShadow const &) { /*call parent op here*/ }
    int & x = this->first;
    double & y = this->second;
};

struct TupleShadow: public std::tuple<int, double> {
    using std::tuple<int, double>::tuple;
    PairShadow & operator=(PairShadow const &) { /*call parent op here*/ }
    int & x = std::get<0>(*this);
    double & y = std::get<1>(*this);
};

auto main() -> int {
    auto twinShadow = PairShadow(1,3.0);
    auto tupleShadow = TupleShadow(1,3.0);
    return tupleShadow.y;
}

或者,也许更好的方法是像@holyblackcat提到的那样,仅为您的结构定义一个重载函数std::get<int>(PairShadow),在许多情况下这样做就足够了,只要您不试图符合特定需要std::pairstd::tuple的不可改变API。


这种方法会带来内存(和可能的性能)开销,并使元组不可分配。在我看来,更好的选择是使用常规的 struct,就像被接受的答案建议的那样,并可能为其专门化 std::get(和相关函数)。 - HolyBlackCat
我非常同意使用常规结构体可能更好,这会带来不必要的内存成本,然而赋值问题可以通过定义赋值运算符来解决,尽管显然会失去简洁性。我会添加一些警告。 - Catskul

1

虽然这是一个老问题,但我会为C++17添加更新。你可以使用结构化绑定声明来命名std::pair和std::tuple的成员。例如:

std::list<std::pair<std::string, long>> getCitiesAndPopulations();
. . . 
for(const auto& [city, population] : getCitiesAndPopulations())
    std::cout << "City: " << city << ", population: " << population << std::endl;

注意:此处未涉及类型安全方面。

0

我认为你应该在这里引入新类型。就拥有一对类而言,我完全支持stl的观点,但这正是java人争论他们不想要一对类的原因,你应该总是为你的一对类似类型引入新类型。

stl解决方案的好处是你可以使用通用类pair,但你可以在真正希望成员以不同方式命名时引入新类型/类。此外,引入新类使你自由地轻松添加第三个成员,如果有必要的话。


-1
也许你可以从pair类继承自己的pair类,并在构造函数中设置两个引用,称为name和zipcode(只需确保实现所有要使用的构造函数)。

4
std::pair没有虚析构函数,因此可能会导致意外的内存泄漏。 - Travis Gockel
1
谢谢。但如果您要引入新类型,为什么不一开始就引入结构体城市 { string name; int zipcode; } 呢? - Ali
@Travis:是的,你应该声明自己的析构函数(它将调用父类的析构函数),以及构造函数。@Ali:通过继承pair,您可以使用其所有功能(例如,当预定义函数需要pair作为参数时,如果您使用从pair继承的yourOwnPair,则编译器将自动将您的类型转换为pair)。 - V_shr
2
那不是问题。如果向类中添加任何需要销毁的内容,那么仅使用std::pair的任何内容都不会调用这些析构函数。http://www.codersource.net/c/c-miscellaneous/c-virtual-destructors.aspx - Travis Gockel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接