mercredi 22 juin 2016

C++ operator<< compile error with gtest AssertionFailure

Please help me understand what is causing the error I describe below.

I wish to abstract a SIMD vector type (4 x single-precision floats) and a naive equivalent (that just uses standard C++ operators - i.e. iterate through vector explicitly) into a single interface, so that they can be used interchangeably and selected at compile-time. Within a namespace I have defined a base class so that my actual implementation classes can use the CRTP pattern (and therefore selectively override functions when necessary, or use the base class's implementation, without invoking a virtual function call). I'm aiming for static (compile-time) polymorphism here, and inlining where possible, as I need this to be fast.

For now I have only defined the index operator, however eventually I will define operators for addition, subtraction, etc, via the CRTP base class, allowing each derived class to override them as appropriate for their storage mechanism.

vector.h:

#ifndef VECTOR_H
#define VECTOR_H

namespace vector {

// CRTP-style base class
template <typename VectorType>
class BaseVector {
 public:
  // provide casts to the derived class pointer
  VectorType & asVector() { return static_cast<VectorType &>(*this); }
  const VectorType & asVector() const { return static_cast<const VectorType &>(*this); }

  float & operator[](size_t i) { return asVector()[i]; }
  // not returning 'const &' because references to SIMD vector elements are not allowed:
  float operator[](size_t i) const { return asVector()[i]; }
};

// The Barton-Nackman trick involves using the CRTP pattern to isolate the following template
// to only those types that are related to BaseVector:
template <typename StreamType, typename VectorType>
StreamType & operator<<(StreamType & s, const BaseVector<VectorType> & v) {
  return s << "[ " << v[0] << ", " << v[1] << ", " << v[2] << ", " << v[3] << " ]";
}

class Vector4f : public BaseVector<Vector4f> {
 public:
  template <typename T>
  Vector4f(T x0, T x1, T x2, T x3) :
      storage_({static_cast<float>(x0),
              static_cast<float>(x1),
              static_cast<float>(x2),
              static_cast<float>(x3)}) {}

  float & operator[](size_t i) { return storage_[i]; }
  const float & operator[](size_t i) const { return storage_[i]; }

 protected:
  typedef std::vector<float> StorageType;
  StorageType storage_;
};

class Vector4f_SIMD : public BaseVector<Vector4f_SIMD> {
 public:

  template <typename T>
  Vector4f_SIMD(T x0, T x1, T x2, T x3) :
      storage_((StorageType){static_cast<float>(x0),
              static_cast<float>(x1),
              static_cast<float>(x2),
              static_cast<float>(x3)}) {}

  float operator[](size_t i) { return storage_[i]; }
  // cannot use references to SIMD vector element
  float operator[](size_t i) const { return storage_[i]; }


 protected:
  typedef float StorageType __attribute__((__vector_size__(16)));
  StorageType storage_;
};

} // namespace vector

#endif // VECTOR_H

I'm compiling with C++11 (clang). Note the use of the Barton-Nackman trick to isolate the template match to only these classes. I'm not 100% sure why this is necessary with C++11, but if I don't use it, the template seems to override the standard operator<< for char * as the second type, creating an ambiguity error, which I don't want to do.

The stream serialisation template seems to work for ostream:

std::cout << vector::Vector4f(1,2,3,4) << std::endl;  // compiles
std::cout << vector::Vector4f_SIMD(1,2,3,4) << std::endl;  // compiles

Where I run into problems is with GoogleTest's AssertionFailure class, that can be used with a stream operator to add information. I'm looking to use a helper function to check that a vector contains the values I expect (without relying on an equality operator that doesn't yet exist):

test_vector.cc:

#include <gtest/gtest.h>
#include "vector.h"

using namespace vector;

// type-parameterized tests
template <typename T>
class TestVector : public ::testing::Test {};

typedef ::testing::Types<vector::Vector4f, vector::Vector4f_SIMD> TypesToTest;
TYPED_TEST_CASE(TestVector, TypesToTest);

template <typename T, typename U>
static ::testing::AssertionResult CheckVector(const T & v, const U & x0, const U & x1, const U & x2, const U & x3) {
  return v[0] == x0 && v[1] == x1 && v[2] == x2 && v[3] == x3 ? ::testing::AssertionSuccess() : ::testing::AssertionFailure() << v << " does not equal [ " << x0 << ", " << x1 << ", " << x2 << ", " << x3 << " ]";
}

TYPED_TEST(TestVector, ctor_by_float_parameters) {
  TypeParam v(0.0f, 0.1f, 0.2f, 0.3f);

  // Note this line causes a compiler error when uncommented:
  //EXPECT_TRUE(CheckVector(v, 0.0, 0.1, 0.2, 0.3));
}

For completeness, I'm compiling this with googletest built in the directory below my source:

$ cd googletest
$ g++ -isystem include -Iinclude -I. -pthread -c src/gtest-all.cc
$ ar -rv libgtest.a gtest-all.o
$ g++ -isystem include -Iinclude -I. -pthread -c src/gtest-main.cc 
$ ar -rv libgtest_main.a gtest_main.o 
$ cd ..
$ g++ -std=c++11 test_vector.cc -isystem googletest/include googletest/libgtest.a googletest/libgtest_main.a -o test_vector

When the EXPECT_TRUE(CheckVector... line above is uncommented, the compiler fails with this first error:

In file included from test_vector.cpp:2:
./vector.hpp:23:10: error: non-const lvalue reference to type 'std::__1::basic_stringstream<char,
      std::__1::char_traits<char>, std::__1::allocator<char> >' cannot bind to a value of unrelated type
      'basic_ostream<char, std::__1::char_traits<char> >'
  return s << "[ " << v[0] << ", " << v[1] << ", " << v[2] << ", " << v[3] << " ]";
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
googletest/include/gtest/gtest-message.h:131:10: note: in instantiation of function template specialization
      'vector::operator<<<std::__1::basic_stringstream<char, std::__1::char_traits<char>, std::__1::allocator<char> >,
      vector::Vector4f_SIMD>' requested here
    *ss_ << val;
         ^
googletest/include/gtest/gtest.h:306:29: note: in instantiation of function template specialization
      'testing::Message::operator<<<vector::Vector4f_SIMD>' requested here
    AppendMessage(Message() << value);
                            ^
test_vector.cpp:15:127: note: in instantiation of function template specialization
      'testing::AssertionResult::operator<<<vector::Vector4f_SIMD>' requested here
  ...&& v[2] == x2 && v[3] == x3 ? ::testing::AssertionSuccess() : ::testing::AssertionFailure() << v << "[ " << x0 <...
                                                                                             ^

test_vector.cpp:20:15: note: in instantiation of function template specialization 'CheckVector' requested here EXPECT_TRUE(CheckVector(v, 0.0, 0.1, 0.2, 0.3));

From what I understand, it is having trouble applying the result of the operator<< template for my Vector base class to the operator<< for the testing::AssertionFailure class. I don't understand why this is causing a problem as it ought to be invoking the AssertionFailure's operator after it has serialised my vector into a stringstream. There's something going on here that I don't yet understand.

Is this problem caused by my use of CRTP, or even the Barton-Nackman trick? Is there a better way to do this and still avoid dynamic polymorphism?

I could avoid using this AssertionFailure class entirely, however I would like to understand what is happening, and how to fix it, rather than just giving up and trying something different.

Any help appreciated, please.

Aucun commentaire:

Enregistrer un commentaire