Intel C ++ cannot convert `T **` to `T const * const *`, GCC can

Problem

Extending Existing Code

I have a digital library that has been designed with one flavor in mind. Now I want to generalize this. The main data structure is the โ€œspinorโ€, โ€‹โ€‹which itself is a multidimensional matrix. There are many functions that are arrays of these spinors. Generalized functions need to take one such spinor array for each flavor.

Let's say there is a function that, at a minimum, does the following:

void copy_spinor(Spinor *out, const Spinor *in) { std::cout << out << " " << in << "\n"; } 

My generalization now is this:

 void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) { std::cout << "Fwd: "; copy_spinor(out[0], in[0]); } 

In real code there is a loop over all num_flav , but it is not so necessary for this demo here.

As far as I understand, you need to read this as const Spinor *(in[num_flav]) , so in is a pointer to an array of possible num_flav elements (or another because foo[] is just *foo in the function parameter) of type a pointer to a link spinor.

The problem is that it does not compile when using Spinor *non_const[2] (without const ), see my previous question . From the answer there I learned that this should not be compiled, because inside the copy_spinor function, a pointer to non_const[0] could point to some const array of Spinor * . Then non_const will point to const data. Therefore it does not work.

My conclusion was that adding another const would make it correct:

 void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) {} 

When I pass my non_const as the second parameter, the function cannot change in[0] to anything, because this pointer is now immutable. It helped me well with GCC 6.3. Now, starting with Intel C ++ 17 production, it no longer works.

Minimum working example:

 #include <cstdint> typedef float Spinor[3][4][2][8]; template <uint8_t num_flav> class Solver { public: void copy_spinor(Spinor *out, const Spinor *in) { std::cout << out << " " << in << "\n"; } void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) { std::cout << "Fwd: "; copy_spinor(out[0], in[0]); } }; int main(int argc, char **argv) { Spinor *s1 = new Spinor[10]; Spinor *s2 = new Spinor[10]; Spinor *s1_a[1] = {s1}; Spinor *s2_a[1] = {s2}; Solver<1> s; s.copy_spinor(s2_a, s1_a); } 

In GCC, it seems to solve the second copy_spinor overload. the s1_a variable, which takes on the role of the previous non_const , is allowed as an argument.

Problems with Intel C ++ 17

Intel C ++ 17, however, does not agree that:

 $ icpc -Wall -pedantic const-spinor-const.cpp --std=c++11 const-spinor-const.cpp(23): error: no instance of overloaded function "Solver<num_flav>::copy_spinor [with num_flav=(uint8_t={unsigned char})'\001']" matches the argument list argument types are: (Spinor *[1], Spinor *[1]) object type is: Solver<(uint8_t)'\001'> s.copy_spinor(s2_a, s1_a); ^ const-spinor-const.cpp(11): note: this candidate was rejected because arguments do not match void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) {} ^ const-spinor-const.cpp(10): note: this candidate was rejected because arguments do not match void copy_spinor(Spinor *out, const Spinor *in) {} ^ 

The error message is not particularly useful because it does not say that the conversion was not allowed. The problem seems to be const .

Perhaps something I missed with Intel C ++? Are there any missing features or have I used the unofficial GCC extension? Is this a bug in Intel C ++ or GCC?

Update: The example also compiles with the current Clang.

Class without template

The same problem persists when the Solver class is not a template class. Since T a[2] matches T a[2] and T *a in the function argument, I can also just write such a function without requiring uint8_t num_flav :

 void copy_spinor(Spinor *out[], const Spinor *const in[]) { std::cout << "Fwd: "; copy_spinor(out[0], in[0]); } 

The error is the same.

Free functions

A similar problem occurs for non-member non-template functions:

 void free_spinor(Spinor *out, const Spinor *in) { std::cout << out << " " << in << "\n"; } void free_spinor(Spinor *out[], const Spinor *const in[]) { std::cout << "Fwd: "; free_spinor(out[0], in[0]); } 

The error message is the same:

 $ icpc -Wall -pedantic const-spinor-const.cpp --std=c++11 const-spinor-const.cpp(97): error: no instance of overloaded function "free_spinor" matches the argument list argument types are: (Spinor *[1], Spinor *[1]) free_spinor(s2_a, s1_a); ^ const-spinor-const.cpp(30): note: this candidate was rejected because arguments do not match void free_spinor(Spinor *out[], const Spinor *const in[]) { ^ const-spinor-const.cpp(26): note: this candidate was rejected because arguments do not match void free_spinor(Spinor *out, const Spinor *in) { ^ 

Attempts to solve

To run the code during production, I see the following options. None of them are particularly attractive.

What would be a good way? I can change my new features with everything I want, but I would like not to touch the caller code as much as possible.

Const wrappers

When I change the definition of s1_a in the main function to have two const , it compiles:

 const Spinor *const s1_a[1] = {s1}; 

Then the copy_spinor function copy_spinor called with the correct argument type.

Each user of generic code must write those const for each function call. It will be very dirty.

Remove Const Correctness.

I can remove the leftmost const from the argument parameters of the function. What compiles on both compilers. However, I want to document that I am not modifying anything in this array, so its values โ€‹โ€‹must be const .

A private solution would be to use some preprocessor constant, which removes const only for the Intel compiler.

 #ifdef __INTEL_COMPILER #define ICPC_CONST #else #define ICPC_CONST const #endif 

Perhaps the user defined some spinor as const. Then I am stuck and should have const there:

 const Spinor *s3_a[1] = {s3}; s.copy_spinor(s2_a, s3_a); 

It is harder to add const than to remove it, so this pretty solution is not enough. Also, the author of the upstream will most likely reject my changes due to a change in his code.

Add non-constant overload for each function

It is possible to add overload for each function. I have two options for a generic function, the second is activated when I work with Intel Compiler:

 void copy_spinor(Spinor *out, const Spinor *in) { std::cout << out << " " << in << "\n"; } void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) { std::cout << "Fwd: "; copy_spinor(out[0], in[0]); } #ifdef __INTEL_COMPILER void copy_spinor(Spinor *out[num_flav], Spinor *const in[num_flav]) { std::cout << "Fwd2: "; copy_spinor(out[0], in[0]); } #endif 

This works well, there is just code duplication. Since my added functions just reuse existing functions, this is not much duplication code. Still a violation of the DRY principle.

Another disadvantage is that the number of overloads is 2^N , where N is the number of const * parameters. There are functions, three arguments like this, so I will need eight instances.

Let the template output Constonst

Testing const Spinor and Spinor possible with the help of templates. I can write a function as a template, so S can be either a data type. Using static_assert will give a slightly more informative error message.

 template <typename S> void copy_spinor(Spinor *out[num_flav], S *const in[num_flav]) { static_assert(std::is_same<Spinor, S>::value || std::is_same<const Spinor, S>::value, "Template parameter must be `const Spinor` or `Spinor`."); std::cout << "Fwd: "; copy_spinor(out[0], in[0]); } 

Ideally, I would like to point out that S can only be a Spinor or const Spinor . Perhaps this is possible with C ++ 14 and above, I should stick with C ++ 11.

This solution looks pretty clean, I can add a template argument and approve for each argument in the function. This will be very good, and there is no code duplication. The only drawback may be a longer compilation time (already quite long, but not very important) and less useful error messages (hopefully static_assert will be covered).

The error message when calling with int ** for GCC is as follows:

 const-spinor-const.cpp: In instantiation of 'void Solver<num_flav>::t_copy_spinor(float (**)[3][4][2][8], S* const*) [with S = int; unsigned char num_flav = 1u; Spinor = float [3][4][2][8]]': const-spinor-const.cpp:86:36: required from here const-spinor-const.cpp:40:9: error: static assertion failed: Template parameter must be `const Spinor` or `Spinor`. static_assert(std::is_same<Spinor, S>::value || ^~~~~~~~~~~~~ const-spinor-const.cpp:45:20: error: no matching function for call to 'Solver<1u>::copy_spinor(float (*&)[3][4][2][8], int* const&)' copy_spinor(out[0], in[0]); ~~~~~~~~~~~^~~~~~~~~~~~~~~ const-spinor-const.cpp:29:10: note: candidate: void Solver<num_flav>::copy_spinor(float (*)[3][4][2][8], const float (*)[3][4][2][8]) [with unsigned char num_flav = 1u; Spinor = float [3][4][2][8]] void copy_spinor(Spinor *out, const Spinor *in) { ^~~~~~~~~~~ const-spinor-const.cpp:29:10: note: no known conversion for argument 2 from 'int* const' to 'const float (*)[3][4][2][8]' const-spinor-const.cpp:33:10: note: candidate: void Solver<num_flav>::copy_spinor(float (**)[3][4][2][8], const float (* const*)[3][4][2][8]) [with unsigned char num_flav = 1u; Spinor = float [3][4][2][8]] void copy_spinor(Spinor *out[num_flav], const Spinor *const in[num_flav]) { ^~~~~~~~~~~ const-spinor-const.cpp:33:10: note: no known conversion for argument 1 from 'float (*)[3][4][2][8]' to 'float (**)[3][4][2][8]' 

The comments indicated the use of enable_if . In this case, my function is as follows:

 template <typename S> typename std::enable_if<std::is_same<const Spinor, const S>::value, void>::type t2_copy_spinor(Spinor *out[num_flav], S *const in[num_flav]) { std::cout << "Fwd: " << typeid(S).name() << " " << typeName<S>() << " "; copy_spinor(out[0], in[0]); } 

It is shorter and possibly more succint. The error message does not contain but my handwritten message is larger. At least the error does not occur in the copy_spinor function, but on the calling site, so the user knows where everything went wrong. Perhaps this is better. And enable_if explains itself to at least the more experienced template users.

 const-spinor-const.cpp: In function 'int main(int, char**)': const-spinor-const.cpp:86:37: error: no matching function for call to 'Solver<1u>::t2_copy_spinor(float (* [1])[3][4][2][8], int* [2])' s.t2_copy_spinor(s2_a, int_array); ^ const-spinor-const.cpp:51:5: note: candidate: template<class S> typename std::enable_if<std::is_same<const float [3][4][2][8], const S>::value, void>::type Solver<num_flav>::t2_copy_spinor(float (**)[3][4][2][8], S* const*) [with S = S; unsigned char num_flav = 1u] t2_copy_spinor(Spinor *out[num_flav], S *const in[num_flav]) { ^~~~~~~~~~~~~~ const-spinor-const.cpp:51:5: note: template argument deduction/substitution failed: const-spinor-const.cpp: In substitution of 'template<class S> typename std::enable_if<std::is_same<const float [3][4][2][8], const S>::value, void>::type Solver<num_flav>::t2_copy_spinor(float (**)[3][4][2][8], S* const*) [with S = int]': const-spinor-const.cpp:86:37: required from here const-spinor-const.cpp:51:5: error: no type named 'type' in 'struct std::enable_if<false, void>' 

The enable_if solution looks better than the static_assert variant.

+7
gcc c ++ 11 icc
source share
1 answer

GCC and clang right here, Intel C ++ is wrong.

Relevant part of the Qualification Conversion Standard [conv.qual] (section number may be 4.4 or 4.5). The wording has changed between C ++ 11 and C ++ 1z (to become C ++ 17) ... but your code, which adds const at several levels, starting from the smallest, is allowed in all versions (C ++ 03, 11 , 14, 1z).

One change is that the multi-level constant rule now applies to pointer arrays, where previously it applied to only a few pointers. But we are actually dealing with several pointers, because the array syntax found in the parameter declaration has the semantics of the pointer.

However, it might be worth a try.

 void copy_spinor(Spinor **out, const Spinor *const *in) 

if the compiler is confused / unable to configure array types to pointer types in the list of function parameters before applying the rule.

+2
source share

All Articles