重载[]运算符以返回变体类型

overload [] operator to return a variant type

本文关键字:类型 返回 运算符 重载      更新时间:2023-10-16

EDIT:多亏了这些答案,我能够用代码解决所有问题。我在这里发布了解决方案:它可能对未来的某个人有用。特别是,使用代理类的建议被证明非常有用!这个例子没有考虑所有的情况,但向变体中添加另一个类型应该是微不足道的

我正在编写一个C++(C11-Linux)自定义类,它的行为有点像无序映射{key,value}。我想重载[]运算符,这样我就可以使用与无序映射相同语法的类:object[key]将返回

问题是我需要object[key]来返回一个变体类型。我可以在内部将存储为字符串或结构,但当我使用对象[key]检索它时,我需要返回的值是intfloat字符串,这取决于运行时确定的某些内部条件

这就是为什么我考虑使用boost::variant库。。。但我愿意接受任何其他建议。唯一的限制是测试类(在示例中)必须编译为共享库。因此,代码必须与C11兼容(我的意思是可由GNU g++4.8.5编译)。

我写了一个简单的例子来展示我想要什么样的行为这个例子没有任何意义。这只是为了说明我所犯的错误。我正在编写的真实类有不同的结构,但bool::variant和运算符[]重载的用法是相同的。

test.cpp

#include <boost/variant.hpp>
typedef boost::variant<int, float> test_t;
class Test
{
int i ;
float f;
void set(int randomint, test_t tmp){
if ( randomint == 0 ) i = boost::get<int>(tmp);
else f = boost::get<float>(tmp);
}
test_t get(int randomint){
if ( randomint == 0 ) return i;
else return f;
}
struct IntOrFloat {
int randomint;
Test *proxy;
explicit operator int () const
{ return boost::get<int>(proxy->get(randomint)); }
void operator= (int tmp)
{ proxy->set(randomint, tmp); }
explicit operator float () const
{ return boost::get<float>(proxy->get(randomint)); }
void operator= (float tmp)
{ proxy->set(randomint, tmp); }
};
public:
IntOrFloat operator [](int randomint)
{ return IntOrFloat{randomint, this}; }
const IntOrFloat operator [](int randomint) const
{ return IntOrFloat{randomint, (Test *) this}; }
};

main.cpp

#include <iostream>
#include <boost/variant.hpp>
#include "test.cpp"
#define INTEGER 0
#define FLOAT 1
int main (void) {
Test test;
int i = 3;
float f = 3.14;
test[INTEGER] = i;
test[FLOAT] = f;
int x = (int) test[INTEGER];
float y = (float) test[FLOAT];
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}

编译并运行

g++ -fPIC -std=c++11 -shared -rdynamic -o test.so test.cpp
g++ -std=c++11 -o test main.cpp -Lpath/to/the/test.so -l:test.so
LD_LIBRARY_PATH="path/to/the/test.so" ./test

在C++中,重载解析不会发生在返回类型上,因此给定

int foo() { return 0; }
float foo() { return 0.f; }

编译器没有认可的方法来区分

int x = foo();
float f = foo();

使用转换运算符重载有一个技巧:

#include <iostream>
struct IntOrFloat {
operator int () const {
std::cout << "returning intn";
return 0;
}
operator float () const {
std::cout << "returning floatn";
return 0.f;
}
};
IntOrFloat foo() { return IntOrFloat(); }
int main () {
int x = foo();
float f = foo();
}

您可以通过使转换explicit:来强制执行更详细的内容

explicit operator int () const ...
explicit operator float () const ...
int x = static_cast<int>(foo()); 
int x = float(foo()); // old-style-cast

这个代理(或其他转换运算符技巧)可以模拟返回类型重载解析。

这个想法曾经出现在搜索支持<euclidian vector> * <euclidian vector>-语法的解决方案时,即operator*,它的意思是点积

向量积最后,它并没有真正实用,也没有对可读性做出积极贡献。更详细的形式dot(vec, vec)cross(vec, vec)更优越,原因有几个,其中:

  • 最小惊奇原则:计算机图形学界习惯于术语"点"answers"交叉">
  • 不那么神秘的错误消息:因为这种代理技术在C++中不是惯用的,所以人们不习惯这种时间间接产生的错误消息
  • 时间和/或空间局部性:本质上是返回一个包含代码的闭包,它可以在许多地方执行多次。这可能会加倍糟糕,因为它不能(实际上,确实)很好地与auto &类型的声明一起工作:

    int main () {
    const auto &f = foo();
    const int g = f;
    const int h = f;
    std::cout << (int)f << "n";
    }
    

    这会多次打印一些东西,同时遵循最不意外的原则。当然,如果你的代理基本上只是转发现成的值,这就不那么严重了。但是错误消息不会变得更好!

注意,您还可以合并模板转换运算符重载和狂野元编程。虽然值得进行一次有趣的实验,但我不想把它放在生产代码库中,因为维护和可读性甚至会降低。

剩下什么?无限的可能性;但一些最可行的:

  • 变量数据类型
  • 元组数据类型(查看std::tuple,它在不同成员类型的情况下附带转换运算符)
  • 不同的习惯用法(例如,命名方法而不是运算符方法)
  • 不同的算法
  • 不同的数据结构
  • 不同的设计模式

当您使用return i时,引擎盖下面发生的事情是创建一个类型为test_t的临时文件,该临时文件封装了int值。这在函数test::test_variant中运行良好,因为返回类型为test_t。这在函数test::operator[]中不起作用,因为返回类型为test_t&。该语言禁止创建对临时的可修改(l-value)引用。

实现这一点的一种方法是将类型为test_t的数据成员添加到类中,测试函数operator[]设置该成员并返回它,而不是返回临时成员。你真正的班级很可能会做一些不同的事情。