7

I recently read cool article: https://akrzemi1.wordpress.com/2015/08/20/can-you-see-the-bug/ Playing with reduced version on ideone I got surprising behaviour:

#include <iostream>
#include <cassert>
using namespace std;
int main() {
    const size_t sz=258;
    string s{sz,'#'};
    assert(2==s.size());
}

does not compile, but same program with const removed compiles:

#include <iostream>
#include <cassert>
using namespace std;
int main() {
    size_t sz=258;
    string s{sz,'#'};
    assert(2==s.size());
}

So my question is this standard required or just compilers decided that one is a compiler warning and other is an compiler error.

If it helps here are the errors and warnings from gcc 5.1 (tnx godbolt)

!!error: narrowing conversion of '258ul' from 'size_t {aka long unsigned int}' to 'char' inside { }


!!warning: narrowing conversion of 'sz' from 'size_t {aka long unsigned int}' to 'char' inside { } [-Wnarrowing]

good guy clang 3.6 gives error in both cases, but the question remains, is one legal and bad C++ and other illegal C++?

MSalters
  • 173,980
  • 10
  • 155
  • 350
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • 3
    Note that the standard only requires a diagnostic. It doesn't distinguish between errors and warnings. The issue was discussed in this bug report: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55783 – Vaughn Cato Aug 28 '15 at 12:07
  • 1
    The underlying problem is that `string s{32,32,32};` should just work. But `32` is an int and int-to-char is a narrowing conversion. We know that `32` can safely be narrowed, precisely because it's a compile-time constant. For this reason compile-time constants are treated differently. (It's not just `const` - `const sz = rand()` is not a compile-time constant and might overflow when narrowed) – MSalters Aug 28 '15 at 13:36

3 Answers3

7

std::string has constructors declared as:

string::string(std::initializer_list<char>);
string::string(std::size_t, char);

When we have list-initialization, the following rule applies:

(N3337 [dcl.init.list]/3): List-initialization of an object or reference of type T is defined as follows:

  • [...]
  • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

The initializer-list constructor is selected due to this rule:

(N3337 [over.match.list]/1): When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.
  • [...]

Now since the initializer-list constructor is the best choice, but a narrowing conversion is required to convert the argument, the program is ill formed.

However, I don't think that makes one compiler correct and one incorrect:

(N3337 [intro.compliance]/8): A conforming implementation may have extensions (including additional library functions), provided they do not alter the behavior of any well-formed program. Implementations are required to diagnose programs that use such extensions that are ill-formed according to this International Standard. Having done so, however, they can compile and execute such programs.

The standard has no concept of a warning vs. an error, so GCC is conformant in issuing a warning diagnostic, then going on to compile the program anyway. Note that GCC will issue an error if you pass -pedantic-errors.

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • both yours and ForEveR answers are nice, but I accepted yours for the last paragraph. – NoSenseEtAl Aug 28 '15 at 12:24
  • Rather than officially recognizing that implementations were sometimes used for purposes where a certain constraint would make the language more useful, and sometimes used for purposes where it would make the language less useful, the C Standard (and later the C++ Standard) decided to allow implementations to either enforce constraints or not, at their leisure, provided only that they issued a diagnostic which they could let programmers ignore. – supercat Jul 09 '21 at 21:55
3

There is narrowing here, since initializer_list<char> constructor is preffered.

N4296 8.5/17.1

— If the initializer is a (non-parenthesized) braced-init-list, the object or reference is list-initialized (8.5.4).

Since string has constructor, that takes initializer_list<char> it will be preffered, since

8.5.4/2

A constructor is an initializer-list constructor if its first parameter is of type std::initializer_list or reference to possibly cv-qualified std::initializer_list for some type E, and either there are no other parameters or else all other parameters have default arguments (8.3.6). [ Note: Initializer-list constructors are favored over other constructors in list-initialization (13.3.1.7).

13.3.1.7/1

When objects of non-aggregate class type T are list-initialized such that 8.5.4 specifies that overload resolution is performed according to the rules in this section, overload resolution selects the constructor in two phases

Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.

and program is ill-formed since

N4296 8.5.4/3.5

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution (13.3, 13.3.1.7). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

Basically gcc will only warn on this snippet:

int main() {
   const size_t v = 258;
   char c = {v};
}

when clang will give error.

ForEveR
  • 55,233
  • 2
  • 119
  • 133
  • But where is the narrowing conversion? `std::string` has a constructor that takes a `size_t` and a `char`, so those are exact matches. – nwp Aug 28 '15 at 11:47
  • 4
    @nwp `std::initializer_list` constructors are greedy, so this calls `string::string(std::initializer_list)` rather than `string::string(std::size_t, char)`. – TartanLlama Aug 28 '15 at 11:48
  • @TartanLlama: Notes may be non-normative by themselves, but a note of the form "[Note: see rule X (13.3.1.7)] can certainly refer to a normative part of the standard. – MSalters Aug 28 '15 at 12:18
  • @MSalters sure, but why not quote the part which is normative? – TartanLlama Aug 28 '15 at 12:20
  • Two common reasons: Often the point to be proven depends on a combination of two related sections of the standard, where one section points to another, and secondly notes are often good summaries. – MSalters Aug 28 '15 at 12:24
3

The problem is the std::string has an initializer lists constructor and it is greedy. {sz,'#'} is being treaated as an initializer list and you are getting a narrowing conversion warning as it is converting sz to a char type to make an std::initializer_list<char>. You can fixe this by calling the constrcutor with () instead of {}

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • I know, but that is not the question. My question is if const makes program illformed or not, see TartanLlama answer. – NoSenseEtAl Aug 28 '15 at 12:23