Repository: cython/cython

Issue:
[BUG] cpp_locals generates broken code for thread local temp variables
**Describe the bug**
When using cpp_locals(True) in combination with  exception handling inside of a prange, an incorrect assignment is generated. The following code:
```cython
cimport cython
from cython.parallel import prange

cdef extern from *:
    """
    class Test {
    };
    """
    cdef cppclass Test:
        Test() nogil except +

@cython.cpp_locals(True)
cdef test():
    cdef int i
    for i in prange(10, nogil=True):
        var = Test()
```
uses std::optional<Test> to store the variable, while it uses Test to store the thread local variable. This results in an invalid assignment:
```
CYTHON_UNUSED __Pyx_Optional_Type<Test> __pyx_v_var;
__Pyx_Optional_Type<Test> __pyx_t_3;
...
Test __pyx_parallel_temp1;
...
__pyx_t_3 = Test();
__pyx_v_var = std::move((*__pyx_t_3));
...
__pyx_parallel_temp1 = __pyx_v_var;
```
here the last assignment to `__pyx_parallel_temp1` is invalid.

**Expected behavior**
The temporary variable `__pyx_parallel_temp1` should use the type `__Pyx_Optional_Type<Test>` as well.


Discussion:
I'd obviously missed that pranges have their own type of temp. I also wonder if the assignment to parallel_temp should use `std::move` too (although that would need a close look at the context it's used in)
> I also wonder if the assignment to parallel_temp should use std::move too (although that would need a close look at the context it's used in)

I wondered this too, since it would help me with my problem from https://github.com/cython/cython/issues/4352.

Resolving patch:
diff --git a/Cython/Compiler/Nodes.py b/Cython/Compiler/Nodes.py
--- a/Cython/Compiler/Nodes.py
+++ b/Cython/Compiler/Nodes.py
@@ -9237,7 +9237,10 @@ def save_parallel_vars(self, code):
             if not lastprivate or entry.type.is_pyobject:
                 continue
 
-            type_decl = entry.type.empty_declaration_code()
+            if entry.type.is_cpp_class and not entry.type.is_fake_reference and code.globalstate.directives['cpp_locals']:
+                type_decl = entry.type.cpp_optional_declaration_code("")
+            else:
+                type_decl = entry.type.empty_declaration_code()
             temp_cname = "__pyx_parallel_temp%d" % temp_count
             private_cname = entry.cname
 
@@ -9251,10 +9254,17 @@ def save_parallel_vars(self, code):
             # Declare the parallel private in the outer block
             c.putln("%s %s%s;" % (type_decl, temp_cname, init))
 
+            self.parallel_private_temps.append((temp_cname, private_cname, entry.type))
+
+            if entry.type.is_cpp_class:
+                # moving is fine because we're quitting the loop and so won't be directly accessing the variable again
+                code.globalstate.use_utility_code(
+                    UtilityCode.load_cached("MoveIfSupported", "CppSupport.cpp"))
+                private_cname = "__PYX_STD_MOVE_IF_SUPPORTED(%s)" % private_cname
             # Initialize before escaping
             code.putln("%s = %s;" % (temp_cname, private_cname))
 
-            self.parallel_private_temps.append((temp_cname, private_cname))
+
 
         code.end_block()  # end critical section
 
@@ -9375,7 +9385,10 @@ def end_parallel_control_flow_block(
             code.putln(
                 "if (%s) {" % Naming.parallel_why)
 
-            for temp_cname, private_cname in self.parallel_private_temps:
+            for temp_cname, private_cname, temp_type in self.parallel_private_temps:
+                if temp_type.is_cpp_class:
+                    # utility code was loaded earlier
+                    temp_cname = "__PYX_STD_MOVE_IF_SUPPORTED(%s)" % temp_cname
                 code.putln("%s = %s;" % (private_cname, temp_cname))
 
             code.putln("switch (%s) {" % Naming.parallel_why)


Test patch:
diff --git a/tests/run/cpp_locals_parallel.pyx b/tests/run/cpp_locals_parallel.pyx
new file mode 100644
--- /dev/null
+++ b/tests/run/cpp_locals_parallel.pyx
@@ -0,0 +1,33 @@
+# mode: run
+# tag: cpp, cpp17, no-cpp-locals, openmp
+# no-cpp-locals because the test is already run with it explicitly set
+
+# cython: cpp_locals=True
+
+from cython.parallel cimport prange
+
+cdef extern from *:
+    """
+    class Test {
+    public:
+        Test() = delete;
+        Test(int v) : value(v) {}
+
+        int get_value() const { return value; }
+    private:
+        int value;
+    };
+    """
+    cdef cppclass Test:
+        Test(int) nogil
+        int get_value()
+
+def test():
+    """
+    >>> test()
+    9
+    """
+    cdef int i
+    for i in prange(10, nogil=True):
+        var = Test(i)
+    print(var.get_value())