我有模板类ImageView<Pixel>
,它存储了一个指向数据和图像大小的非拥有指针。
我希望有常量的正确性,所以我正在使用Pixel
和const Pixel
:
std::byte * data;
ImageView<char> img(data, width, height);
std::byte const* cdata;
ImageView<char> img(cdata, width, height); // compile error
ImageView<const char> cimg(cdata, width, height);
但是,当然,这会导致这样的代码出现问题:
void foo(ImageView<const char> const&);
ImageView<char> view;
foo(view); // conversion from ImageView<char> to ImageView<const char> const& required
显而易见的解决方案是使用构造函数添加隐式转换:
template <class = std::enable_if_t<std::is_const_v<Pixel>>>
constexpr ImageView(ImageView<std::remove_const_t<Pixel>> const& other) noexcept
: m_data(other.GetData())
, m_stride(other.GetStride())
, m_width(other.GetWidth())
, m_height(other.GetHeight())
{}
但它的缺点是在每次转换时创建临时的,ImageView
在大多数 64 位平台上是 24 字节。此临时版本与原始临时文件仅在类型上有所不同 - 它们具有完全相同的布局。所以我开始考虑使用reinterpret_cast
和 const 引用转换运算符:
template <class = std::enable_if_t<!std::is_const_v<Pixel>>>
constexpr operator ImageView<std::add_const_t<Pixel>> const&() const noexcept
{
using ConstImageView = ImageView<std::add_const_t<Pixel>>;
return *reinterpret_cast<ConstImageView const*>(this);
}
它似乎有效,但我不确定最后一个片段的正确性。
整个类有简化版本(仅省略了一些额外的非虚函数):
template <class Pixel>
class ImageView
{
template <class T, class U>
using copy_const_qualifier =
std::conditional_t<
std::is_const_v<T>,
std::add_const_t<U>,
std::remove_const_t<U>>;
using Byte = copy_const_qualifier<Pixel, std::byte>;
public:
constexpr ImageView(Byte * data, unsigned w, unsigned h, std::size_t s) noexcept
: m_data(data)
, m_stride(s)
, m_width(w)
, m_height(h)
{}
constexpr Byte * GetData() const noexcept { return m_data; }
constexpr std::size_t GetStride() const noexcept { return m_stride; }
constexpr unsigned GetWidth() const noexcept { return m_width; }
constexpr unsigned GetHeight() const noexcept { return m_height; }
protected:
Byte * m_data;
std::size_t m_stride; // in bytes
unsigned m_width; // in pixels
unsigned m_height; // in pixels
};
是的,该reinterpret_cast
无效,您不能只将一个对象强制转换为另一个不相关类型的对象。好吧,你可以,但请不要从船尾访问它。
您可以添加转换运算符,而不是禁用隐式构造函数,该构造函数不起作用,因为您在非重载解析上下文中使用 SFINAE(有一些解决方法,例如使条件依赖于可实现相同目标)。但是使用转换运算符更清洁 IMO:
operator ImageView<const Pixel>() { return {m_data, m_width, m_height, m_stride}; }
您不必担心复制,编译器很聪明! :)24字节真的没什么可担心的。
gcc 在-O1
及以上版本上生成相同的代码,用于将ImageView<const char>
和ImageView<char>
传递给foo
,以及-O2
上方的 clang 。因此,如果您使用优化进行编译,则完全没有区别。
即使const char
可以隐式转换为char
,ImageView<char>
和ImageView<const char>
是完全不相关的类型,我在这里什么也没学到。但这是一种耻辱,因为从某种意义上说,ImageView<const char>
是可以修改的ImageView<char>
。
幸运的是,我们有一个工具来调用编译器的东西是别的东西。这是继承的定义(根据利斯科夫规则)。就是这样。让ImageView<const char>
继承ImageView<char>
解决你的大部分问题,这是有道理的:
template<class T>
struct ImageView {};
template<class T>
struct ImageView<const T> : ImageView<T>
{};
void f(ImageView<char>&) {}
void f_const(ImageView<const char>&) {}
int main()
{
ImageView<char> d1;
ImageView<const char> d2;
f(d1);
f(d2);
//f_const(d1); // error: invalid initialization of reference of type 'ImageView<const char>&' from expression of type 'ImageView<char>'
f_const(d2);
}
演示
事实上,使用 @YCS 的想法可以实现所需的行为,但它需要更复杂的代码和const_cast
才能与m_data
一起使用。const_cast
这里是安全的,因为非常量ImageView
的构造函数仅接受指向非常量数据的指针。
所以现在,我将保留带有转换构造函数或运算符的版本。如果我注意到临时对性能的重大影响,我将返回以下代码:
template <class Pixel>
struct ImageView : public ImageView<const Pixel>
{
constexpr ImageView(Pixel * data) noexcept
: ImageView(data)
{}
constexpr Pixel * GetData() const noexcept
{
const Pixel * data = ImageView<const Pixel>::GetData();
return const_cast<Pixel*>(data);
}
};
template <class Pixel>
struct ImageView<const Pixel>
{
constexpr ImageView(const Pixel * data) noexcept
: m_data(data)
{}
constexpr const Pixel * GetData() const noexcept
{
return m_data;
}
private:
const Pixel * m_data;
};
int main()
{
int * data = nullptr;
const int * cdata = nullptr;
ImageView<int> img(data);
//ImageView<int> img1(cdata); // compile error
ImageView<const int> cimg(data);
ImageView<const int> cimg1(cdata);
auto img2 = img;
auto cimg2 = cimg;
ImageView<const int> cimg3(img);
ImageView<const int> cimg4 = static_cast<ImageView<const int>>(img);
ImageView<const int> cimg5 = img;
img.GetData();
cimg.GetData();
return 0;
}