5

I was trying to write a templated base class to store a fixed number of data types, each with varying length. Here is a simplified version of much what I was trying to do:

template< int NINT, int NR0 >
class EncapsulatedObjectBase
{
   public:

  EncapsulatedObjectBase();

  ~EncapsulatedObjectBase();

  double m_real[NR0];
  int m_int[NINT];
}

Yeah...so the template parameters can be zero, thus declaring a zero-length array of objects. There will be multiple derived classes for this base, each defining their own number of variables. I have two questions:

1) Is this approach fundamentally flawed?

2) If so...why doesn't icc13 or gcc4.7.2 give me warnings about this when I instantiate a zero-length array? For gcc I use -wall and -wextra -wabi. The lack of warnings made me think that this sort of thing was OK.

EDIT:

Here is the contents of a file that show what I am talking about:

#include <iostream>

template< int NINT, int NR0 >
class EncapsulatedObjectBase
{
public:
  EncapsulatedObjectBase(){}
  ~EncapsulatedObjectBase(){}

  double m_real[NR0];
  int m_int[NINT];
};


class DerivedDataObject1 : public EncapsulatedObjectBase<2,0>
{
   public:

   DerivedDataObject1(){}

  ~DerivedDataObject1(){}

  inline int& intvar1() { return this->m_int[0]; }
  inline int& intvar2() { return this->m_int[1]; }

};


class DerivedDataObject2 : public EncapsulatedObjectBase<0,2>
{
   public:

   DerivedDataObject2(){}

  ~DerivedDataObject2(){}

  inline double& realvar1() { return this->m_real[0]; }
  inline double& realvar2() { return this->m_real[1]; }
};




int main()
{
   DerivedDataObject1 obj1;
   DerivedDataObject2 obj2;

   obj1.intvar1() = 12;
   obj1.intvar2() = 5;

   obj2.realvar1() = 1.0e5;
   obj2.realvar2() = 1.0e6;

   std::cout<<"obj1.intvar1()  = "<<obj1.intvar1()<<std::endl;
   std::cout<<"obj1.intvar2()  = "<<obj1.intvar2()<<std::endl;
   std::cout<<"obj2.realvar1() = "<<obj2.realvar1()<<std::endl;
   std::cout<<"obj2.realvar2() = "<<obj2.realvar2()<<std::endl;


}

If I compile this with "g++ -Wall -Wextra -Wabi main.cpp" I get no warnings. I have to use the -pedantic flag to get warnings. So I still don't know how unsafe this is. In retrospect, I feel as though it must not be a very good idea...although it would be pretty useful if I could get away with it.

doc07b5
  • 600
  • 1
  • 7
  • 18
  • What compiler and version are you using? gcc 4.7.2 [complains](http://liveworkspace.org/code/1pAqHQ$2) about the zero-length array. – Praetorian Jan 18 '13 at 00:57
  • @Praetorian: gcc 4.7.2. I think that it might have something to do with the template? – doc07b5 Jan 18 '13 at 01:04
  • 1
    _Warning_? It is an **error** to define a _0-sized_ array – K-ballo Jan 18 '13 at 01:08
  • @doc07b5: So, what exactly are you compiling? In my experiments GCC 4.7.2 generates errors (in `-pedantic-errors` mode) when trying to instantiate this template with zeros as arguments. – AnT stands with Russia Jan 18 '13 at 01:10
  • 1
    @K-ballo: the standard never requires an error. An implementation is permitted to accept code that fails to conform to syntactic constraints, provided that it "diagnoses" the error. The meaning of the code is up to the implementation. A warning is an acceptable diagnostic as far as the standard is concerned, and that's why you need `-pedantic-errors` to tell gcc to reject certain ill-formed constructs. – Steve Jessop Jan 18 '13 at 01:15
  • @AndreyT: I wasn't using the pendantic flags. I better put up a better description of what I am really doing. Sorry...my first post. – doc07b5 Jan 18 '13 at 01:17
  • Assuming you want to allow the template to instantiate with zero-valued parameters, the solution is probably to declare the member as a `std::array`. That has a special case to allow size zero. If you want the code to be C++03-compatible then there's `boost:array`. If you don't want to change the code that uses the data members, work out what operations you actually perform on the array data member, and implement your own class template that provides those operations, with a partial specialization for size 0. It could be all you need is `operator[]`. – Steve Jessop Jan 18 '13 at 10:46

3 Answers3

3

In C using a zero-sized array as the last member of a struct is actually legal and is commonly used when the struct is going to end up with some sort of dynamically-created inline data that's not known at compile-time. In other words, I might have something like

struct MyData {
    size_t size;
    char data[0];
};

struct MyData *newData(size_t size) {
    struct MyData *myData = (struct MyData *)malloc(sizeof(struct MyData) + size);
    myData->size = size;
    bzero(myData->data, size);
    return myData;
}

and now the myData->data field can be accessed as a pointer to the dynamically-sized data

That said, I don't know how applicable this technique is to C++. But it's probably fine as long as you never subclass your class.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • Isn't it more common to create an array member of size 1 for the struct hack? – Praetorian Jan 18 '13 at 00:57
  • This example might be clearer if you didn't reuse the name `data` – Mel Nicholson Jan 18 '13 at 00:58
  • @Praetorian: Probably a matter of style. But if you use a size of 1 then you have to remember to subtract that 1 from the size you want the member to have. – Lily Ballard Jan 18 '13 at 00:58
  • @KevinBallard See ildjarn's comment [here](http://stackoverflow.com/questions/295027/array-of-zero-length#comment13008154_295033) – Praetorian Jan 18 '13 at 00:58
  • @MelNicholson: Fair enough. Updated. – Lily Ballard Jan 18 '13 at 00:58
  • @Praetorian: Interesting. If it's actually illegal according to the standards, then why does OP's code work? – Lily Ballard Jan 18 '13 at 00:59
  • Firstly, creating a zero-sized array has never been legal in C. Which is why the proper C89/90 technique for variable sized array was to using size `1` not `0`. Some compilers allowed using `0` as a completely non-standard language extension. Secondly, the question is obviosuly about C++. – AnT stands with Russia Jan 18 '13 at 01:02
  • Clang 3.2 accepts a zero-length array both in C99 mode and in C++11 mode. – Lily Ballard Jan 18 '13 at 01:04
  • @Kevin Ballard: You don't have to "remember to subtract 1". All you need to do is to use the proper memory allocation idiom `myData = malloc(offsetof(struct MyData, data) + size * sizeof *myData->data)`. This allocation technique is completely independent of the actual size used in the tail array declaration. – AnT stands with Russia Jan 18 '13 at 01:05
  • @Kevin Ballard: Accepting zero-sized array is, again, nothing more than a non-standard extension kept in the compiler to support poorly written legacy code. – AnT stands with Russia Jan 18 '13 at 01:05
  • 1
    Hrm, it turns out that `-pedantic` causes warnings about zero-length arrays, in both Clang and GCC 4.7.2, but without that, they both accept it silently. – Lily Ballard Jan 18 '13 at 01:06
  • @Kevin Ballard: Which is why one has to specify `-pedantic-errors` when using GCC for checking the validity of the code. Or use Comeau Online. – AnT stands with Russia Jan 18 '13 at 01:07
  • C99 introduced support for size-less tail array declarations specifically to avoid the illegal `[0]` array declarations and at the same time support the code that uses `sizeof` in such allocations (instead of `offsetof`). – AnT stands with Russia Jan 18 '13 at 01:08
  • 2
    Btw, C99 introduces "flexible array members" as the sanctioned way to do this, defined in 6.7.2.1/16. It does something that a size-1 array doesn't do, which is to explicitly state in the standard that you're allowed to index off the end of the array provided that you don't exceed the memory actually allocated. This indexing was supported by compiler-writers (it would take effort not to support it, given how array indexing is implemented in practice), but whether C89 strictly speaking guaranteed that it worked was the kind of thing language lawyers love to angst over. – Steve Jessop Jan 18 '13 at 01:08
  • @SteveJessop: Oh that's cute. I like that. – Lily Ballard Jan 18 '13 at 01:11
  • Just for those who didn't know, the correct way to do the struct hack in C++ is to make an array of a very large size (at least as large as you will ever need) as the last object in a struct and allocate fewer bytes than necessary. If you do it that way you have no UB. – Seth Carnegie Jan 18 '13 at 01:20
  • Hi all...so am not really understanding how to connect the dots between the discussion of the "struct hack", and what I am trying to do. The zero length arrays in my case may not be the last member of a class. Can someone fill in the dots for me? Thanks...much appreciated. – doc07b5 Jan 18 '13 at 01:46
  • @doc07b5: If you want a zero-length array to be a warning, use the `-pedantic` flag. If you want it to be an error, use `-pedantic-errors`. – Lily Ballard Jan 18 '13 at 01:54
  • 2
    @doc07b5: Alternatively if you don't like using `-pedantic`, you could provide a template specialization for `NINT = 0` and do something there. – Lily Ballard Jan 18 '13 at 01:56
  • @KevinBallard: Good point...That seems to be the "safe" way to go. Unfortunately this example is a simplified case of my implementation. I actually have about 5 data types that I am holding...so that means I would need to provide about...ohhh...30 specializations!! – doc07b5 Jan 18 '13 at 02:13
  • @doc07b5: Why do you need that? Only specialize on `NINT`, leave the rest as unbound template parameters. – Lily Ballard Jan 18 '13 at 05:34
  • @KevinBallard: Because each data array can have zero length. And if I have 5 data types, thus 5 data arrays, then I would have to account for every combination of those 5 data lengths being 0. Each specialization would have to be missing the data member that has zero length....unless I am missing something here? If not, then there are 30 permutations that one can combine for zero lengths in each of the 5 arrays. I think that this is pretty trivial to generate all those specializations using a perl script working on the original template declaration. – doc07b5 Jan 18 '13 at 08:31
  • @doc07b5: You only need 5 specializations, one for each variable. They're only going to be ambiguous if you try to instantiate the template with multiple `0`s, which is a compiler error, which is what you want anyway. – Lily Ballard Jan 18 '13 at 09:24
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/22990/discussion-between-doc07b5-and-kevin-ballard) – doc07b5 Jan 18 '13 at 20:04
3

Zero-sized arrays are actually illegal in C++:

[C++11: 8.3.4/1]: [..] If the constant-expression (5.19) is present, it shall be an integral constant expression and its value shall be greater than zero. The constant expression specifies the bound of (number of elements in) the array. If the value of the constant expression is N, the array has N elements numbered 0 to N-1, and the type of the identifier of D is “derived-declarator-type-list array of N T”. [..]

For this reason, your class template cannot be instantiated with arguments 0,0 in GCC 4.1.2 nor in GCC 4.7.2 with reasonable flags:

template< int NINT, int NR0 >
class EncapsulatedObjectBase
{
   public:

  EncapsulatedObjectBase();

  ~EncapsulatedObjectBase();

  double m_real[NR0];
  int m_int[NINT];
};

int main()
{
   EncapsulatedObjectBase<0,0> obj;
}

t.cpp: In instantiation of 'EncapsulatedObjectBase<0, 0>':
t.cpp:17: instantiated from here
Line 10: error: ISO C++ forbids zero-size array
compilation terminated due to -Wfatal-errors.

clang 3.2 says:

source.cpp:10:17: warning: zero size arrays are an extension [-Wzero-length-array]

(Note that, in any case, you won't get any error until you do try to instantiate such a class.)

So, is it a good idea? No, not really. I'd recommend prohibiting instantiation for your class template when either argument is 0. I'd also look at why you want to have zero-length arrays and consider adjusting your design.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • 3
    It is illegal, but it can be instantiated with G++'s default options (as your own liveworkspace output shows.) You should be clear that when you say "reasonable flags" you're referring to `-pedantic-errors -Wfatal-errors`, which many people consider unreasonable for general use. – Jonathan Wakely Jan 18 '13 at 01:32
  • 1
    @Lightness Races in Orbit: Thanks for the advice. The design is a little "fast and loose". – doc07b5 Jan 18 '13 at 02:25
  • @JonathanWakely: I don't consider any combination of flags that permits illegal code to be "reasonable". That's my definition. – Lightness Races in Orbit Jan 18 '13 at 02:38
  • So for example you won't use `long long` in C++03 code? That seems a bit ... pedantic. Not to mention impractical in plenty of real world code. – Jonathan Wakely Jan 18 '13 at 09:05
  • 1
    @LightnessRacesinOrbit: it's your definition, but it's not the standard's definition of conformance, so you have to expect that people won't understand what you mean by "reasonable" unless you elaborate :-) Note that `-pedantic-errors` does *not* achieve the goal that one might hope for, of accepting only code that all conforming implementations accept (i.e. "checking your code is portable"). For example, it accepts `int test_array[9 - CHAR_BIT];`, which is non-portable because it relies on an implementation-defined quantity. – Steve Jessop Jan 18 '13 at 10:52
1

1) Add to declaration of your class C++11 static_assert or BOOST_STATIC_ASSERT and you will have compile-time diagnostic for zero length array:

....
   BOOST_STATIC_ASSERT(NR0 > 0);
   BOOST_STATIC_ASSERT(NINT > 0);
   double m_real[NR0];
   int m_int[NINT];
};

2) Use std::array or boost::array and you will have run-time diagnostic (in debug mode) for index overflow problem in such code:

   BOOST_STATIC_ASSERT(NR0 > 0);
   BOOST_STATIC_ASSERT(NINT > 0);
   boost::array<double, NR> m_real;   //double m_real[NR0];
   boost::array<int, NINT> m_int;     //int m_int[NINT];
};

Remark: class boost::array has specialisation for zero-size array

3) Use size_t but not int for size of array.

Your design is quite dangerous:

   DerivedDataObject1 a;
   a.m_real[2] = 1;   // size of m_real == 0 !!!

I think it will better to change design of your class EncapsulatedObjectBase. May be it will better to use:

   template<typename T, size_t N> class EncapsulatedObjectBase
   {
    ....
   };
   class DerivedDataObject1 : public EncapsulatedObjectBase<int,2>
   {
     ....
   };
   class DerivedDataObject2 : public EncapsulatedObjectBase<double,2>
   {
     ....
   };
   class DerivedDataObject3 : public EncapsulatedObjectBase<double,2>
                            , public EncapsulatedObjectBase<int,2>
   {
     ....
   };
SergV
  • 1,269
  • 8
  • 20