microsoft / GSL

Guidelines Support Library
Other
6.11k stars 736 forks source link

gsl::not_null at compile time #1102

Closed jonnygrant closed 6 months ago

jonnygrant commented 1 year ago

Has anyone found an easy way to get not_null to fire off at build time (for those cases that it can see are obvious) ? Would be great if not_null could work at build time. Currently it just calls terminate at runtime.

Pasted the code into Godbolt to demonstrate this example where I pasted GSL not_null : https://godbolt.org/z/oqrTffTYj

I did find one way to get a compile time warning. I call a function with an attribute warning. And I also leave out that function so it fails to link.

:

g++ -O1

I can stop the build using this approach below. Unfortunately it only works in an optimized build.

In function 'void f2(const char*)',
    inlined from 'int main()' at <source>:14:7:
<source>:8:46: warning: call to 'nullptr_compile_abort' declared with attribute warning: nullptr compile error [-Wattribute-warning]
    8 |     if (str == nullptr) nullptr_compile_abort();
      |                         ~~~~~~~~~~~~~~~~~~~~~^~

#if __OPTIMIZE__
void nullptr_compile_abort() __attribute__((warning("nullptr compile error")));
#endif

static void f2(const char * str)
{
#if __OPTIMIZE__
    if (str == nullptr) nullptr_compile_abort();
#endif
}

int main()
{
    f2((const char *)nullptr);
}
jonnygrant commented 1 year ago

I tried strict_not_null

but it also didn't give what I need. https://godbolt.org/z/nET6nse31

beinhaerter commented 1 year ago

I tried strict_not_null

but it also didn't give what I need. https://godbolt.org/z/nET6nse31

That is expected. The difference between strict_not_null and not_null is that strict_not_null has an explicit constructor whereas not_null has a constructor that can be called implicitely. And that difference is not relevant for what you want to achieve.

dmitrykobets-msft commented 1 year ago

Hi @jonnygrant,

There are some compile-time checks that are offered by GSL not_null, such as disallowing construction directly from the nullptr (i.e., f(nullptr);), but these are implemented via C++ language features (such as deleting the constructor taking std::nullptr_t). Otherwise, all checks intentionally occur at runtime.

More comprehensive compile-time checks would be best achieved via a static analysis tool that understands gsl::not_null. For example MSVC currently has some experimental gsl::not_null support that would raise a compile-time diagnostic on your code example.

jonnygrant commented 1 year ago

Hi @dmitrykobets-msft Many thanks for your reply.

Thank you for the static analysis tool suggestion. It's a good idea. I'm only on GNU/Linux gcc, so I don' think I could compile that unfortunately.

I did patch not_null locally, and now the build stops in an optimized build when it sees my int* is NULL

Example uses the following. It adds a dummy method, just to trigger the warning -Wnotnull __attribute__ ((nonnull));

https://godbolt.org/z/xd1YeE5cs

Would you consider integrating something similar into the official GSL not_null class?

jonnygrant commented 1 year ago

This might be better using

__attribute__((error("not_null found nullptr")));

https://godbolt.org/z/cMvjqb93T

Could you consider adding this. It works with GCC and Clang.

beinhaerter commented 1 year ago

In your godbolt with error I changed the definition of test in main to int * test = new (std::nothrow) int();. That code still does not compile because the pointer might be null and the compiler might need to call compile_error_null_check. So with this proposed solution legitimate code does not compile.

jonnygrant commented 1 year ago

@beinhaerter

I tried your suggestion with GCC and found the same, however with clang, it did compile https://godbolt.org/z/63jYWW794

Same with malloc(8), clang compiled but not gcc. https://godbolt.org/z/hve7hha9x

Maybe the __attribute__ ((nonnull));

example is better I provided above.

beinhaerter commented 1 year ago

Your __attribute__((nonnull)) change also fails with GCC and int * test = new (std::nothrow) int();.

jonnygrant commented 1 year ago

Your __attribute__((nonnull)) change also fails with GCC and int * test = new (std::nothrow) int();.

May I ask, my idea is to get a not_null build error when a pointer is passed that might be nullptr. So it seems to be working as expected?

You get a nice compile time error when accidentally passing a value that might be a nullptr as a not_null<int*>

In constructor 'constexpr gsl::not_null<T>::not_null(T) [with <template-parameter-2-1> = void; T = int*]',
    inlined from 'int main()' at <source>:475:6:
<source>:236:53: error: argument 1 null where non-null expected [-Werror=nonnull]

For such a fatal memory allocation failure by new, I'd put some handler in to log it and abort()

if(nullptr == test)
{
    log_error("FATAL new (std::nothrow) int(); returned nullptr\n");
    abort();
}

Oddly this attribute nonnull example doesn't work in the else {}, even after checking the pointer is valid. Sharing the link https://godbolt.org/z/7661ffdxK

jonnygrant commented 1 year ago

In your godbolt with error I changed the definition of test in main to int * test = new (std::nothrow) int();. That code still does not compile because the pointer might be null and the compiler might need to call compile_error_null_check. So with this proposed solution legitimate code does not compile.

I updated this error example after getting the build error for the possible nullptr returned by operator new, so this works well

https://godbolt.org/z/Kh6Wa3oTM

dmitrykobets-msft commented 1 year ago

@jonnygrant thanks for continuing the investigation and iterating on the design.

With your latest changes, this example fails to compile: https://godbolt.org/z/o6oj63Pd1, whereas in the existing GSL implementation it will compile and run fine if the pointer is not null, and runtime error otherwise.

int main()
{
    gsl::not_null<int*> test = new (std::nothrow) int();
    int a = *test;
    (void)a; // suppress unused var warning
}

In fact, even the following code will fail the compile-time check: https://godbolt.org/z/4TG1aP3os

void foo(int* ptr)
{
   gsl::not_null<int*> nn  = ptr; // compiler can't assert that ptr != nullptr, compile_error_null_check
   (void)nn; // suppress unused var warning
}
int main() {}

So the programming model effectively proposed with these changes requires the users of not_null to manually insert nullchecks:

void foo(int* ptr)
{
   if (ptr == nullptr) return;
   gsl::not_null<int*> nn  = ptr; // compiles fine
   (void)nn; // suppress unused var warning
}
int main() {}

If this is intended, then you will probably want to first open a CoreGuidelines issue to discuss this proposal, since it is a non-trivial deviation from the current programming model suggested by not_null.

jonnygrant commented 1 year ago

@dmitrykobets-msft Many thanks for your reply and the suggestion, I've filed on CppCoreGuidelines now. Let's see what they say.

You're right nullptr checks would need to be inserted in some application code. My idea gives the programmer chance to evaluate where the nullptr came from at build time, and what logic caused it.

hsutter commented 6 months ago

Maintainers call: Closed per https://github.com/isocpp/CppCoreGuidelines/issues/2071